#  NanoVNASaver
#
#  A python program to view and export Touchstone data from a NanoVNA
#  Copyright (C) 2019, 2020 Rune B. Broberg
#  Copyright (C) 2020ff NanoVNA-Saver Authors
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
import itertools as it
import math
from typing import Callable, List, Tuple

import numpy as np
# pylint: disable=import-error, no-name-in-module
from scipy.signal import find_peaks

from NanoVNASaver.RFTools import Datapoint


def zero_crossings(data: List[float]) -> List[int]:
    """find zero crossings

    Args:
        data (List[float]): data list execute

    Returns:
        List[int]: sorted indices of zero crossing points
    """
    if not data:
        return []

    np_data = np.array(data)

    # start with real zeros (ignore first and last element)
    real_zeros = [n for n in np.where(np_data == 0.0)[0] if
                  n not in {0, np_data.size - 1}]
    # now multipy elements to find change in signess
    crossings = [
        n if abs(np_data[n]) < abs(np_data[n + 1]) else n + 1
        for n in np.where((np_data[:-1] * np_data[1:]) < 0.0)[0]
    ]
    return sorted(real_zeros + crossings)


def maxima(data: List[float], threshold: float = 0.0) -> List[int]:
    """maxima

    Args:
        data (List[float]): data list to execute

    Returns:
        List[int]: indices of maxima
    """
    peaks = find_peaks(
        data, width=2, distance=3, prominence=1)[0].tolist()
    return [
        i for i in peaks if data[i] > threshold
    ] if threshold else peaks


def minima(data: List[float], threshold: float = 0.0) -> List[int]:
    """minima

    Args:
        data (List[float]): data list to execute

    Returns:
        List[int]: indices of minima
    """
    bottoms = find_peaks(
        -np.array(data), width=2, distance=3, prominence=1)[0].tolist()
    return [
        i for i in bottoms if data[i] < threshold
    ] if threshold else bottoms


def take_from_idx(data: List[float],
                  idx: int,
                  predicate: Callable) -> List[int]:
    """take_from_center

    Args:
        data (List[float]): data list to execute
        idx (int): index of a start position
        predicate (Callable): predicate on which elements to take
            from center. (e.g. lambda i: i[1] < threshold)

    Returns:
        List[int]: indices of element matching predicate left
                   and right from index
    """
    lower = list(reversed(
        [i for i, _ in
         it.takewhile(predicate,
                      reversed(list(enumerate(data[:idx]))))]))
    upper = [i for i, _ in
             it.takewhile(predicate,
                          enumerate(data[idx:], idx))]
    return lower + upper


def center_from_idx(gains: List[float],
                    idx: int, delta: float = 3.0) -> int:
    """find maximum from index postion of gains in a attn dB gain span

    Args:
        gains (List[float]): gain values
        idx (int): start position to search from
        delta (float, optional): max gain delta from start. Defaults to 3.0.

    Returns:
        int: position of highest gain from start in range (-1 if no data)
    """
    peak_db = gains[idx]
    rng = take_from_idx(gains, idx,
                        lambda i: abs(peak_db - i[1]) < delta)
    return max(rng, key=lambda i: gains[i]) if rng else -1


def cut_off_left(gains: List[float], idx: int,
                 peak_gain: float, attn: float = 3.0) -> int:
    """find first position in list where gain in attn lower then peak
    left from index

    Args:
        gains (List[float]): gain values
        idx (int): start position to search from
        peak_gain (float): reference gain value
        attn (float, optional): attenuation to search position for.
                                Defaults to 3.0.

    Returns:
        int: position of attenuation point. (-1 if no data)
    """
    return next(
        (i for i in range(idx, -1, -1) if
            (peak_gain - gains[i]) > attn),
        -1)


def cut_off_right(gains: List[float], idx: int,
                  peak_gain: float, attn: float = 3.0) -> int:
    """find first position in list where gain in attn lower then peak
    right from index

    Args:
        gains (List[float]): gain values
        idx (int): start position to search from
        peak_gain (float): reference gain value
        attn (float, optional): attenuation to search position for.
                                Defaults to 3.0.

    Returns:
        int: position of attenuation point. (-1 if no data)
    """

    return next(
        (i for i in range(idx, len(gains)) if
            (peak_gain - gains[i]) > attn),
        -1)


def dip_cut_offs(gains: List[float], peak_gain: float,
                 attn: float = 3.0) -> Tuple[int, int]:
    rng = np.where(np.array(gains) < (peak_gain - attn))[0].tolist()
    return (rng[0], rng[-1]) if rng else (math.nan, math.nan)


def calculate_rolloff(s21: List[Datapoint],
                      idx_1: int, idx_2: int) -> Tuple[float, float]:
    if idx_1 == idx_2:
        return (math.nan, math.nan)
    freq_1, freq_2 = s21[idx_1].freq, s21[idx_2].freq
    gain_1, gain_2 = s21[idx_1].gain, s21[idx_2].gain
    factor = freq_1 / freq_2 if freq_1 > freq_2 else freq_2 / freq_1
    attn = abs(gain_1 - gain_2)
    decade_attn = attn / math.log10(factor)
    octave_attn = decade_attn * math.log10(2)
    return (octave_attn, decade_attn)
