"""

Compute statistics for localization data.

These values can represent new properties of locdata.

"""
from __future__ import annotations

from collections import namedtuple

import numpy as np
import pandas as pd

from locan.data.locdata import LocData

__all__ = ["statistics", "ranges", "range_from_collection"]


def statistics(
    locdata, statistic_keys=("count", "min", "max", "mean", "median", "std", "sem")
):
    """
    Compute selected statistical parameter for localization data.

    Parameters
    ----------
    locdata : LocData | pandas.DataFrame | pandas.Series
        Localization data

    statistic_keys : str | tuple[str]
        Pandas statistic functions.
        Default: ('count', 'min', 'max', 'mean', 'median', 'std', 'sem')

    Returns
    -------
    dict
        A dict with descriptive statistics.
    """

    if isinstance(locdata, LocData):
        data = locdata.data
    elif isinstance(locdata, (pd.DataFrame, pd.Series)):
        data = locdata
    else:
        raise TypeError("locdata should be of type locan.LocData or pandas.DataFrame.")

    statistics_ = data.agg(statistic_keys)

    if isinstance(locdata, pd.Series):
        p = str(data.name)
        if isinstance(statistic_keys, str):
            dict_ = {p + "_" + statistic_keys: statistics_}
        else:
            dict_ = {p + "_" + s: statistics_[s] for s in statistic_keys}
    else:
        if isinstance(statistic_keys, str):
            generator = (p for p in list(data))
            dict_ = {p + "_" + statistic_keys: statistics_[p] for p in generator}
        else:
            generator = ((p, s) for p in list(data) for s in statistic_keys)
            dict_ = {p + "_" + s: statistics_[p][s] for p, s in generator}

    return dict_


def ranges(locdata, loc_properties=None, special=None, epsilon=1):
    """
    Provide data ranges for localization properties.
    If LocData is empty, None is returned.
    If LocData carries a single localization, the range will be
    (value, value + `epsilon`).

    Parameters
    ----------
    locdata : LocData
        Localization data.
    loc_properties : str | tuple[str, ...] | list[str] | True | None
        Localization properties for which the range is determined.
        If None the ranges for all spatial coordinates are returned.
        If True the ranges for all localization properties are returned.
    special : str | None
        If None (min, max) ranges are determined from data and returned;
        if 'zero' (0, max) ranges with max determined from data are returned.
        if 'link' (min_all, max_all) ranges with min and max determined from
        all combined data are returned.
    epsilon : float
        number to specify the range for single values in locdata.

    Returns
    -------
    numpy.ndarray[float] | None
        The data range (min, max) for each localization property.
        Array of shape (dimension, 2).
    """
    if locdata.data.empty:
        return None
    elif len(locdata) == 1:
        pass

    if loc_properties is None:
        ranges_ = locdata.bounding_box.hull.T.copy()
    elif loc_properties is True:
        ranges_ = np.array([locdata.data.min(), locdata.data.max()]).T
    elif isinstance(loc_properties, str):
        ranges_ = np.array(
            [[locdata.data[loc_properties].min(), locdata.data[loc_properties].max()]]
        )
    else:
        loc_properties = list(loc_properties)
        ranges_ = np.array(
            [locdata.data[loc_properties].min(), locdata.data[loc_properties].max()]
        ).T

    if len(locdata) == 1:
        if ranges_.size == 0:
            ranges_ = np.concatenate(
                [locdata.coordinates, locdata.coordinates + epsilon], axis=0
            ).T
        else:
            ranges_ = ranges_ + [0, epsilon]

    if special is None:
        pass
    elif special == "zero":
        ranges_[:, 0] = 0
    elif special == "link":
        minmax = np.array([ranges_[:, 0].min(axis=0), ranges_[:, 1].max(axis=0)])
        ranges_ = np.repeat(minmax[None, :], len(ranges_), axis=0)
    else:
        raise ValueError(f"The parameter special={special} is not defined.")

    return ranges_


def range_from_collection(locdatas, loc_properties=None, special=None, epsilon=1):
    """
    Compute the maximum range from all combined localizations for each
    dimension.

    Parameters
    ----------
    locdatas : list[LocData]
        Collection of localization datasets.
    loc_properties : str | tuple[str, ...] | list[str] | True | None
        Localization properties for which the range is determined.
        If None the ranges for all spatial coordinates are returned.
        If True the ranges for all locdata.data properties are returned.
    special : str | None
        If None (min, max) ranges are determined from data and returned;
        if 'zero' (0, max) ranges with max determined from data are returned.
        if 'link' (min_all, max_all) ranges with min and max determined from
        all combined data are returned.
    epsilon : float
        number to specify the range for single values in locdata.

    Returns
    -------
    namedtuple
        A namedtuple('Ranges', locdata.coordinate_labels)
        of namedtuple('Range', 'min max').
    """
    ranges_ = [
        ranges(
            locdata=locdata,
            loc_properties=loc_properties,
            special=special,
            epsilon=epsilon,
        )
        for locdata in locdatas
    ]

    mins = np.min([rand[:, 0] for rand in ranges_], axis=0)
    maxs = np.max([rand[:, 1] for rand in ranges_], axis=0)

    if loc_properties is None:
        labels = locdatas[0].coordinate_labels
    else:
        labels = loc_properties

    Ranges = namedtuple("Ranges", labels)
    Range = namedtuple("Range", "min max")
    result = Ranges(
        *(Range(min_value, max_value) for min_value, max_value in zip(mins, maxs))
    )
    return result
