import os
import re
import numpy as np
from pathlib import Path
from datetime import datetime
from typing import overload, Any

import xarray as xr
from scipy.signal import savgol_filter
from scipy.integrate import cumulative_trapezoid as cumtrapz
from .types import LidarInfoType, Telescope

from gfatpy.utils.io import read_yaml

""" MODULE For General Lidar Utilities
"""

# LIDAR SYSTEM INFO
INFO_FILE = Path(__file__).parent.parent.absolute() / "info" / "info_lidars.yml"
LIDAR_INFO: LidarInfoType = read_yaml(INFO_FILE)

INFO_PLOT_FILE = Path(__file__).parent.parent.absolute() / "plot" / "info.yml"
LIDAR_PLOT_INFO = read_yaml(INFO_PLOT_FILE)


@overload
def signal_to_rcs(signal: xr.DataArray, ranges: xr.DataArray) -> xr.DataArray:
    ...


@overload
def signal_to_rcs(
    signal: np.ndarray[Any, np.dtype[np.float64]],
    ranges: np.ndarray[Any, np.dtype[np.float64]],
) -> np.ndarray[Any, np.dtype[np.float64]]:
    ...


def signal_to_rcs(signal, ranges):
    """Convert Lidar Signal to range-corrected signal

    Args:
        signal (np.ndarray[Any, np.dtype[np.float64]] | xr.DataArray): Lidar signal
        ranges (np.ndarray[Any, np.dtype[np.float64]] | xr.DataArray): Lidar ranges of signal

    Returns:
         xr.DataArray | np.ndarray[Any, np.dtype[np.float64]]: Range-corrected signal

    """
    return signal * ranges**2


@overload
def rcs_to_signal(rcs: xr.DataArray, ranges: xr.DataArray) -> xr.DataArray:
    ...


@overload
def rcs_to_signal(rcs: np.ndarray, ranges: np.ndarray) -> np.ndarray:
    ...


def rcs_to_signal(rcs, ranges):
    return rcs / ranges**2


def smooth_signal(signal, method="savgol", savgol_kwargs: dict | None = None):
    """Smooth Lidar Signal

    Args:
        signal ([type]): [description]
        method (str, optional): [description]. Defaults to 'savgol'.
    """

    if method == "savgol":
        if savgol_kwargs is None:
            savgol_kwargs = {"window_length": 21, "polyorder": 2}
        smoothed_signal = savgol_filter(signal, **savgol_kwargs)
    else:
        raise NotImplementedError(f"{method} has not been implemented yet")

    return smoothed_signal


def estimate_snr(signal, window=5):
    """[summary]

    Args:
        signal ([type]): [description]
    """

    # ventana: numero impar
    if window % 2 == 0:
        window += 1
    subw = window // 2

    n = len(signal)
    avg = np.zeros(n) * np.nan
    std = np.zeros(n) * np.nan

    for i in range(n):
        ind_delta_min = i - subw if i - subw >= 0 else 0
        ind_delta_max = i + subw if i + subw < n else n - 1

        si = signal[ind_delta_min : (ind_delta_max + 1)]
        avg[i] = np.nanmean(si)
        std[i] = np.nanstd(si)

        # print("%i, %i, %i" % (i, ind_delta_min, ind_delta_max + 1))
        # print(signal[ind_delta_min:(ind_delta_max+1)])
    snr = avg / std

    return snr, avg, std


def get_lidar_name_from_filename(fn):
    """Get Lidar System Name from L1a File Name
    Args:
        fn (function): [description]
    """
    lidar_nick = os.path.basename(fn).split("_")[0]
    if lidar_nick in LIDAR_INFO["metadata"]["nick2name"].keys():
        lidar = lidar_nick
    else:
        lidar = None
    return lidar


def to_licel_date_str(date: datetime) -> str:
    # Convert month number into hex capital letter
    month_hex = f"{date.month:x}".upper()
    return f'{date.strftime(r"%y")}{month_hex}{date.strftime(r"%d")}'


def licel_to_datetime(licel_name: str) -> datetime:
    """Convert Licel Date String to datetime
    Args:
        licel_name (str): Licel Date String
    """
    name, extension = licel_name.split(".")
    # Convert hexadecimal 'm' to decimal
    month_decimal = int(name[-5], 16)
    return datetime.strptime(
        f"{name[-7:-5]}{month_decimal:02d}{name[-4:-2]}T{name[-2:]}{extension[:4]}",
        r"%y%m%dT%H%M%S",
    )


def filter_wildcard(
    directory: Path, wildcard: str = r"\.\d+$", recursive_search=True
) -> list[Path]:
    """Filter files by wildcard

    Args:
        directory (Path): Directory to search.
        wildcard (str, optional): Wildcard. Defaults to r'\.\d+$'.
        recursive_search (bool, optional): Recursive search. Defaults to True means recursive search includes subdirectories.

    Raises:
        Exception: Directory does not exist.
        Exception: Directory is not a directory.

    Returns:
        list[Path]: List of files.
    """
    if not directory.exists():
        raise Exception(f"Directory {directory} does not exist")
    if not directory.is_dir():
        raise Exception(f"{directory} is not a directory")
    if recursive_search:
        return [file for file in directory.rglob("*") if re.search(wildcard, file.name)]
    else:
        return [file for file in directory.glob("*") if re.search(wildcard, file.name)]


def get_532_from_telescope(telescope: Telescope = Telescope.xf) -> str:
    if telescope == telescope.xf:
        return "532xpa"
    elif telescope == telescope.ff:
        return "532fpa"
    elif telescope == telescope.nf:
        return "532npa"

    raise ValueError("Telescope type not recognized. Options are xf, ff, nf")


def sigmoid(x, x0, k, coeff: float = 1, offset: float = 0):
    y = 1 / (1 + np.exp(-k * (x - x0)))
    return (coeff * y) + offset


def extrapolate_beta_with_angstrom(
    beta_ref: np.ndarray,
    wavelength_ref: float,
    wavelength_target: float,
    angstrom_exponent: float | np.ndarray,
) -> np.ndarray[Any, np.dtype[np.float64]]:
    return beta_ref * (wavelength_target / wavelength_ref) ** -angstrom_exponent


def integrate_from_reference(integrand, x, reference_index):
    """

    at x[ref_index], the integral equals = 0
    """
    # integrate above reference
    int_above_ref = cumtrapz(integrand[reference_index:], x=x[reference_index:])

    # integrate below reference
    int_below_ref = cumtrapz(
        integrand[: reference_index + 1][::-1], x=x[: reference_index + 1][::-1]
    )[::-1]

    return np.concatenate([int_below_ref, np.zeros(1), int_above_ref])


def optical_depth(extinction, height, ref_index=0):
    """
    Integrate extinction profile along height: r'$\tau(z) = \int_0^z d\dseta \alpha(\dseta)$'
    """

    return integrate_from_reference(extinction, height, reference_index=ref_index)


def refill_overlap(
    atmospheric_profile: np.ndarray[Any, np.dtype[np.float64]],
    height: np.ndarray[Any, np.dtype[np.float64]] | xr.DataArray,
    fulloverlap_height: float = 600,
    fill_with: float | None = None,
) -> np.ndarray[Any, np.dtype[np.float64]]:
    """Fill overlap region [0-`fulloverlap_height`] of the profile `atmospheric_profile` with the value `fill_with` provided by the user. If None, fill with the value at `fulloverlap_height`.

    Args:
        atmospheric_profile (np.ndarray): Atmospheric profile
        height (np.ndarray): Range profile in meters
        fulloverlap_height (float, optional): Fulloverlap height in meters. Defaults to 600 m.
        fill_with (float, optional): Value to fill the overlap region. Defaults to None.

    Returns:
        np.ndarray: Profile `atmospheric_profile` with the overlap region filled.
    """
    if isinstance(atmospheric_profile, xr.DataArray):
        atmospheric_profile = atmospheric_profile.values

    if fulloverlap_height < height[0] or fulloverlap_height > height[-1]:
        raise ValueError(
            "The fulloverlap_height is outside the range of height values."
        )

    idx_overlap = np.abs(height - fulloverlap_height).argmin()

    if fill_with is None:
        fill_with = atmospheric_profile[idx_overlap]

    new_profile = np.copy(atmospheric_profile)
    new_profile[:idx_overlap] = fill_with

    return new_profile
