from itertools import permutations

import numpy as np
import nolds

from .utils import apply_sliding_window

def approximate_entropy(signal=None, window_size=None, overlap=None, windows=None, m=2, r=0.2):
    """
    Calculate the approximate entropy for each window in the signal.

    :param signal: The input signal as a 1D array or list (if windows not provided).
    :type signal: array-like
    :param window_size: The number of samples in each window (if windows not provided).
    :type window_size: int
    :param overlap: The number of samples to overlap between consecutive windows (if windows not provided).
    :type overlap: int
    :param windows: Optional precomputed list of windows.
    :type windows: list
    :param m: Embedding dimension for the entropy calculation.
    :type m: int
    :param r: Tolerance (a fraction of the standard deviation) for the entropy calculation.
    :type r: float
    :return: 1D array of approximate entropy values for each window.
    :rtype: numpy.ndarray

    Reference: https://en.wikipedia.org/wiki/Approximate_entropy
    """
    def approx_entropy_func(x):
        N = len(x)
        def _phi(mm):
            x_embedded = np.array([x[i:i+mm] for i in range(N - mm + 1)])
            C = np.sum(np.abs(x_embedded[:, None] - x_embedded[None, :]).max(axis=2) <= r, axis=0) / (N - mm + 1)
            return np.sum(np.log(C)) / (N - mm + 1)
        return abs(_phi(m) - _phi(m+1))
    return apply_sliding_window(approx_entropy_func, windows=windows, signal=signal, window_size=window_size, overlap=overlap)


def sample_entropy(signal=None, window_size=None, overlap=None, windows=None, m=2, r=0.2):
    """
    Calculate the sample entropy for each window in the signal.

    :param signal: The input signal as a 1D array or list (if windows not provided).
    :type signal: array-like
    :param window_size: The number of samples in each window (if windows not provided).
    :type window_size: int
    :param overlap: The number of samples to overlap between consecutive windows (if windows not provided).
    :type overlap: int
    :param windows: Optional precomputed list of windows.
    :type windows: list
    :param m: Embedding dimension for the entropy calculation.
    :type m: int
    :param r: Tolerance for the entropy calculation.
    :type r: float
    :return: 1D array of sample entropy values for each window.
    :rtype: numpy.ndarray

    Reference: https://en.wikipedia.org/wiki/Sample_entropy
    """
    return apply_sliding_window(
        lambda x: nolds.sampen(x, emb_dim=m, tolerance=r),
        windows=windows, signal=signal, window_size=window_size, overlap=overlap
    )


def shannon_entropy(signal=None, window_size=None, overlap=None, windows=None, bins=50):
    """
    Calculate the Shannon entropy for each window in the signal.

    :param signal: The input signal as a 1D array or list (if windows not provided).
    :type signal: array-like
    :param window_size: The number of samples in each window (if windows not provided).
    :type window_size: int
    :param overlap: The number of samples to overlap between consecutive windows (if windows not provided).
    :type overlap: int
    :param windows: Optional precomputed list of windows.
    :type windows: list
    :param bins: Number of bins to use when estimating the probability distribution.
    :type bins: int
    :return: 1D array of Shannon entropy values for each window.
    :rtype: numpy.ndarray

    Reference: https://en.wikipedia.org/wiki/Entropy_(information_theory)
    """
    def entropy_func(x):
        hist, _ = np.histogram(x, bins=bins, density=True)
        hist = hist + 1e-12  # Avoid log of zero
        return -np.sum(hist * np.log2(hist))
    return apply_sliding_window(entropy_func, windows=windows, signal=signal, window_size=window_size, overlap=overlap)


def lempel_ziv_complexity(signal=None, window_size=None, overlap=None, windows=None):
    """
    Calculate the Lempel-Ziv complexity for each window in the signal.
    (Note: This example reuses `nolds.sampen` as a placeholder. A proper LZ complexity
    implementation would differ.)

    :param signal: The input signal as a 1D array or list (if windows not provided).
    :type signal: array-like
    :param window_size: The number of samples in each window (if windows not provided).
    :type window_size: int
    :param overlap: The number of samples to overlap between consecutive windows (if windows not provided).
    :type overlap: int
    :param windows: Optional precomputed list of windows.
    :type windows: list
    :return: 1D array of Lempel-Ziv complexity values for each window.
    :rtype: numpy.ndarray

    Reference: https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv_complexity
    """
    return apply_sliding_window(
        lambda x: nolds.sampen(x),  # Placeholder implementation
        windows=windows, signal=signal, window_size=window_size, overlap=overlap
    )


def permutation_entropy(signal=None, window_size=None, overlap=None, windows=None, order=3, delay=1):
    """
    Calculate the permutation entropy for each window in the signal.

    Permutation entropy measures the complexity of the time series based on the
    probabilities of ordinal patterns (i.e., the order relations between values) in the data.

    :param signal: The input signal as a 1D array or list (if windows not provided).
    :type signal: array-like
    :param window_size: The number of samples in each window (if windows not provided).
    :type window_size: int
    :param overlap: The number of samples to overlap between consecutive windows (if windows not provided).
    :type overlap: int
    :param windows: Optional precomputed list of windows.
    :type windows: list
    :param order: The embedding dimension (order) used to form ordinal patterns.
    :type order: int
    :param delay: The time delay used to construct ordinal patterns.
    :type delay: int
    :return: 1D array of permutation entropy values for each window.
    :rtype: numpy.ndarray

    Reference: https://en.wikipedia.org/wiki/Permutation_entropy
    """
    def pe(x, order, delay):
        n = len(x)
        if n < delay * (order - 1) + 1:
            return np.nan
        ordinal_patterns = list(permutations(range(order)))
        pattern_counts = {pattern: 0 for pattern in ordinal_patterns}
        for i in range(n - delay * (order - 1)):
            subseq = x[i: i + delay * order: delay]
            pattern = tuple(np.argsort(subseq))
            pattern_counts[pattern] += 1
        total = sum(pattern_counts.values())
        if total == 0:
            return 0
        probs = np.array(list(pattern_counts.values())) / total
        probs = probs[probs > 0]
        return -np.sum(probs * np.log2(probs))
    return apply_sliding_window(
        lambda x: pe(x, order, delay),
        windows=windows, signal=signal, window_size=window_size, overlap=overlap
    )


def largest_lyapunov_exponent(signal=None, window_size=None, overlap=None, windows=None, emb_dim=6):
    """
    Calculate the largest Lyapunov exponent for each window in the signal.

    :param signal: The input signal as a 1D array or list (if windows not provided).
    :type signal: array-like
    :param window_size: The number of samples in each window (if windows not provided).
    :type window_size: int
    :param overlap: The number of samples to overlap between consecutive windows (if windows not provided).
    :type overlap: int
    :param windows: Optional precomputed list of windows.
    :type windows: list
    :param emb_dim: Embedding dimension for phase-space reconstruction.
    :type emb_dim: int
    :return: 1D array of the largest Lyapunov exponent values for each window.
    :rtype: numpy.ndarray

    Reference: https://en.wikipedia.org/wiki/Lyapunov_exponent
    """
    return apply_sliding_window(
        lambda x: nolds.lyap_r(x, emb_dim=emb_dim),
        windows=windows, signal=signal, window_size=window_size, overlap=overlap
    )


def mobility(signal=None, window_size=None, overlap=None, windows=None):
    """
    Calculate the mobility of the signal within each window, defined as the square root of the variance
    of the first derivative divided by the variance of the original signal.

    :param signal: The input signal as a 1D array or list (if windows not provided).
    :type signal: array-like
    :param window_size: The number of samples in each window (if windows not provided).
    :type window_size: int
    :param overlap: The number of samples to overlap between consecutive windows (if windows not provided).
    :type overlap: int
    :param windows: Optional precomputed list of windows.
    :type windows: list
    :return: 1D array of mobility values for each window.
    :rtype: numpy.ndarray

    Reference: https://en.wikipedia.org/wiki/Mobility_(physics)
    """
    return apply_sliding_window(
        lambda x: np.sqrt(np.var(np.diff(x)) / np.var(x)),
        windows=windows, signal=signal, window_size=window_size, overlap=overlap
    )
