# -*- coding: utf-8 -*-
"""Transforming arrays of Mantarray data throughout the analysis pipeline."""
from typing import Any
from typing import Dict
from typing import List
from typing import Union
import uuid

from nptyping import NDArray
import numpy as np
from scipy import signal

from .constants import BESSEL_BANDPASS_UUID
from .constants import BESSEL_LOWPASS_10_UUID
from .constants import BESSEL_LOWPASS_30_UUID
from .constants import BUTTERWORTH_LOWPASS_30_UUID
from .constants import CENTIMILLISECONDS_PER_SECOND
from .constants import RAW_TO_SIGNED_CONVERSION_VALUE
from .exceptions import FilterCreationNotImplementedError
from .exceptions import UnrecognizedFilterUuidError

FILTER_CHARACTERISTICS: Dict[uuid.UUID, Dict[str, Union[str, float, int]]] = {
    BESSEL_BANDPASS_UUID: {
        "filter_type": "bessel",
        "order": 4,
        "high_pass_hz": 0.1,
        "low_pass_hz": 10,
    },
    BESSEL_LOWPASS_10_UUID: {"filter_type": "bessel", "order": 4, "low_pass_hz": 10},
    BESSEL_LOWPASS_30_UUID: {"filter_type": "bessel", "order": 4, "low_pass_hz": 30},
    BUTTERWORTH_LOWPASS_30_UUID: {
        "filter_type": "butterworth",
        "order": 4,
        "low_pass_hz": 30,
    },
}


def create_filter(
    filter_uuid: uuid.UUID, sample_period_centimilliseconds: int,
) -> NDArray[(Any, Any), float]:
    """Create a filter to apply to data streams.

    Args:
        filter_uuid: a UUID of an already accepted and approved filter
        sample_period_centimilliseconds: the sampling period for the data stream you want to apply the filter to

    Returns:
        The scipy 'b' and 'a' vectors to use in scipy.signal.filtfilt
    """
    sampling_frequency_hz = 1 / (
        sample_period_centimilliseconds / CENTIMILLISECONDS_PER_SECOND
    )
    nyquist_frequency_limit = sampling_frequency_hz / 2

    if filter_uuid not in FILTER_CHARACTERISTICS:
        raise UnrecognizedFilterUuidError(filter_uuid)

    the_filter_characteristics = FILTER_CHARACTERISTICS[filter_uuid]
    normalized_high_pass_frequency: Union[int, float]
    normalized_low_pass_frequency: Union[int, float]
    filter_order: int
    if "order" in the_filter_characteristics:
        if not isinstance(the_filter_characteristics["order"], int):
            raise NotImplementedError("The filter order must always be an int.")

        filter_order = the_filter_characteristics["order"]
    pass_boundaries: List[Union[float, int]] = list()
    bandpass_type = "lowpass"  # by default
    if "high_pass_hz" in the_filter_characteristics:
        if isinstance(the_filter_characteristics["high_pass_hz"], str):
            raise NotImplementedError(
                "The high pass frequency should never be a string."
            )
        normalized_high_pass_frequency = (
            the_filter_characteristics["high_pass_hz"] / nyquist_frequency_limit
        )
        pass_boundaries.append(normalized_high_pass_frequency)
    if "low_pass_hz" in the_filter_characteristics:
        if isinstance(the_filter_characteristics["low_pass_hz"], str):
            raise NotImplementedError(
                "The low pass frequency should never be a string."
            )
        normalized_low_pass_frequency = (
            the_filter_characteristics["low_pass_hz"] / nyquist_frequency_limit
        )
        pass_boundaries.append(normalized_low_pass_frequency)

    if len(pass_boundaries) == 2:
        bandpass_type = "bandpass"
    filter_type = the_filter_characteristics["filter_type"]
    if filter_type == "bessel":
        sos_polys = signal.bessel(
            filter_order, pass_boundaries, btype=bandpass_type, output="sos"
        )
    elif filter_type == "butterworth":
        sos_polys = signal.butter(
            filter_order, pass_boundaries, btype=bandpass_type, output="sos"
        )
    else:
        raise FilterCreationNotImplementedError(filter_uuid)

    if not isinstance(sos_polys, NDArray[float]):
        raise NotImplementedError("Returned polynomials most be float arrays")
    return sos_polys


def apply_sensitivity_calibration(
    raw_gmr_reading: NDArray[(2, Any), int]
) -> NDArray[(2, Any), int]:
    """Apply the result of a sensor sensitivity calibration.

    Actual sensitivity calibration will be performed once information obtained from Jason.

    Args:
        raw_gmr_reading: an original 2d array of time vs GMR readings. Could be from tissue construct or reference sensor.

    Returns:
        A 2d array of the time vs GMR readings after sensitivity calibration. Data will be rounded to integers if calibration results in slight decimal behavior.
    """
    return raw_gmr_reading


def noise_cancellation(
    tissue_gmr_reading: NDArray[(2, Any), int],
    reference_gmr_reading: NDArray[
        (2, Any), int
    ],  # pylint: disable=unused-argument # this will eventually be used
) -> NDArray[(2, Any), int]:
    """Perform cancellation of ambient magnetic noise using reference sensor.

    This should be performed after sensitivity calibration has been applied to the raw data from each sensor.

    Args:
        tissue_gmr_reading: from the tissue construct sensor
        reference_gmr_reading: from the reference sensor

    Returns:
        A single 2D array of time vs GMR reading.
    """
    return tissue_gmr_reading


def apply_empty_plate_calibration(
    noise_cancelled_gmr: NDArray[(2, Any), int],
) -> NDArray[(2, Any), int]:
    """Apply the result of an empty plate calibration.

    Actual empty plate calibration will be performed once information obtained from Jason.

    Args:
        noise_cancelled_gmr: an 2D array of Time and GMR readings after combining reference and tissue sensor readings.

    Returns:
        A 2D array of the Time and GMR readings after empty plate calibration. Data will be rounded to integers if calibration results in slight decimal behavior.
    """
    return noise_cancelled_gmr


def apply_noise_filtering(
    fully_calibrated_gmr: NDArray[(2, Any), int],
    scipy_filter_sos_coefficients: NDArray[(Any, Any), float],
) -> NDArray[(2, Any), int]:
    """Apply the result of an empty plate calibration.

    Actual empty plate calibration will be performed once information obtained from Jason.

    Args:
        fully_calibrated_gmr: an 2D array of Time and GMR readings after applying the Empty Plate calibration.
        scipy_filter_sos_coefficients: The 'second order system' coefficient array that scipy filters generate when created

    Returns:
        A 2D array of the Time and GMR readings after empty plate calibration. Data will be rounded to integers if calibration results in slight decimal behavior.
    """
    time_readings = fully_calibrated_gmr[0, :]
    gmr_readings = fully_calibrated_gmr[1, :]

    float_array = signal.sosfiltfilt(scipy_filter_sos_coefficients, gmr_readings)
    int_array = np.rint(float_array).astype(np.int32)

    filtered_data: NDArray[(2, Any), int] = np.vstack((time_readings, int_array))
    return filtered_data


def calculate_voltage_from_gmr(
    gmr_data: NDArray[(2, Any), int],
    reference_voltage: Union[float, int] = 3.3,
    adc_gain: int = 32,
) -> NDArray[(2, Any), np.float32]:
    """Convert 'signed' 24-bit values from an ADC to measured voltage.

    Args:
        gmr_data: time and GMR numpy array. Typically coming from filtered_gmr_data
        reference_voltage: Almost always leave as default of 3.3V
        adc_gain: Current implementation of Mantarray is constant value of 32, but may change in the future. Value can be obtained from HDF5 metadata

    Returns:
        A 2D array of time vs Voltage
    """
    voltage = (
        gmr_data[1, :].astype(np.float32)
        * (1 / RAW_TO_SIGNED_CONVERSION_VALUE)
        * (reference_voltage / adc_gain)
    )
    return np.vstack((gmr_data[0, :].astype(np.float32), voltage))


def calculate_displacement_from_voltage(
    voltage_data: NDArray[(2, Any), np.float32],
) -> NDArray[(2, Any), np.float32]:
    """Convert voltage to displacement.

    Args:
        voltage_data: time and Voltage numpy array. Typically coming from calculate_voltage_from_gmr

    Returns:
        A 2D array of time vs Displacement
    """
    voltage = voltage_data[1, :]
    time = voltage_data[0, :]
    displacement = voltage * -1 + 0.1  # simplistically just invert the data for now
    return np.vstack((time, displacement)).astype(np.float32)
