"""
This module implements flexible oversampler queries.
"""

import inspect

import numpy as np

from ..base import OverSampling, SimplexSamplingMixin
from ..noise_removal import NoiseFilter
from ..oversampling import (
    A_SUWO,
    ADASYN,
    ADG,
    ADOMS,
    AHC,
    AMSCO,
    AND_SMOTE,
    ANS,
    ASMOBD,
    Assembled_SMOTE,
    Borderline_SMOTE1,
    Borderline_SMOTE2,
    CBSO,
    CCR,
    CE_SMOTE,
    cluster_SMOTE,
    CURE_SMOTE,
    DBSMOTE,
    DE_oversampling,
    DEAGO,
    distance_SMOTE,
    DSMOTE,
    DSRBF,
    E_SMOTE,
    Edge_Det_SMOTE,
    G_SMOTE,
    GASMOTE,
    Gaussian_SMOTE,
    polynom_fit_SMOTE_star,
    polynom_fit_SMOTE_bus,
    polynom_fit_SMOTE_poly,
    polynom_fit_SMOTE_mesh,
    Gazzah,
    IPADE_ID,
    ISMOTE,
    ISOMAP_Hybrid,
    KernelADASYN,
    kmeans_SMOTE,
    Lee,
    LLE_SMOTE,
    LN_SMOTE,
    LVQ_SMOTE,
    MCT,
    MDO,
    MOT2LD,
    MSYN,
    MWMOTE,
    NDO_sampling,
    NEATER,
    NRAS,
    NRSBoundary_SMOTE,
    NT_SMOTE,
    OUPS,
    PDFOS,
    ProWSyn,
    Random_SMOTE,
    ROSE,
    RWO_sampling,
    Safe_Level_SMOTE,
    SDSMOTE,
    Selected_SMOTE,
    SL_graph_SMOTE,
    SMMO,
    SMOBD,
    SMOTE_Cosine,
    SMOTE_D,
    SMOTE_ENN,
    SMOTE_FRST_2T,
    SMOTE_IPF,
    SMOTE_OUT,
    SMOTE_PSO,
    SMOTE_PSOBAT,
    SMOTE_RSB,
    SMOTE_TomekLinks,
    SMOTE,
    SN_SMOTE,
    SOI_CJ,
    SOMO,
    SPY,
    SSO,
    Stefanowski,
    SUNDO,
    Supervised_SMOTE,
    SVM_balance,
    SYMPROD,
    TRIM_SMOTE,
    V_SYNTH,
    VIS_RST,
    MSMOTE,
    SMOTE_AMSR,
    SMOTEWB,
)

from ._runtimes import runtimes

__all__ = [
    "get_all_oversamplers",
    "get_metric_learning_oversamplers",
    "get_simplex_sampling_oversamplers",
    "get_multiclass_oversamplers",
    "get_proportion_oversamplers",
    "get_all_noisefilters",
    "generate_parameter_combinations",
]


def determine_n_quickest(oversamplers, n_quickest):
    """
    Determine the n quickest oversamplers.

    Args:
        oversamplers (list): a list of oversampler classes
        n_quickest (int): the number of quickest to be determined

    Returns:
        list: the n quickest oversamplers
    """

    if n_quickest == -1:
        return oversamplers

    pairs = [(osampler, runtimes[osampler.__name__]) for osampler in oversamplers]

    sorted_pairs = sorted(pairs, key=lambda x: x[1])

    n_quickest = np.min([n_quickest, len(oversamplers)])

    return [pair[0] for pair in sorted_pairs[:n_quickest]]


def get_all_oversamplers(n_quickest=-1):
    """
    Returns all oversampling classes

    Args:
        n_quickest (int): the number of quickest oversamplers to return

    Returns:
        list(OverSampling): list of all oversampling classes

    Example::

        import smote_variants as sv

        oversamplers= sv.get_all_oversamplers()
    """

    oversamplers = [
        A_SUWO,
        ADASYN,
        ADG,
        ADOMS,
        AHC,
        AMSCO,
        AND_SMOTE,
        ANS,
        ASMOBD,
        Assembled_SMOTE,
        Borderline_SMOTE1,
        Borderline_SMOTE2,
        CBSO,
        CCR,
        CE_SMOTE,
        cluster_SMOTE,
        CURE_SMOTE,
        DBSMOTE,
        DE_oversampling,
        DEAGO,
        distance_SMOTE,
        DSMOTE,
        DSRBF,
        E_SMOTE,
        Edge_Det_SMOTE,
        G_SMOTE,
        GASMOTE,
        Gaussian_SMOTE,
        polynom_fit_SMOTE_star,
        polynom_fit_SMOTE_bus,
        polynom_fit_SMOTE_poly,
        polynom_fit_SMOTE_mesh,
        Gazzah,
        IPADE_ID,
        ISMOTE,
        ISOMAP_Hybrid,
        KernelADASYN,
        kmeans_SMOTE,
        Lee,
        LLE_SMOTE,
        LN_SMOTE,
        LVQ_SMOTE,
        MCT,
        MDO,
        MOT2LD,
        MSYN,
        MWMOTE,
        NDO_sampling,
        NEATER,
        NRAS,
        NRSBoundary_SMOTE,
        NT_SMOTE,
        OUPS,
        PDFOS,
        ProWSyn,
        Random_SMOTE,
        ROSE,
        RWO_sampling,
        Safe_Level_SMOTE,
        SDSMOTE,
        Selected_SMOTE,
        SL_graph_SMOTE,
        SMMO,
        SMOBD,
        SMOTE_Cosine,
        SMOTE_D,
        SMOTE_ENN,
        SMOTE_FRST_2T,
        SMOTE_IPF,
        SMOTE_OUT,
        SMOTE_PSO,
        SMOTE_PSOBAT,
        SMOTE_RSB,
        SMOTE_TomekLinks,
        SMOTE,
        SN_SMOTE,
        SOI_CJ,
        SOMO,
        SPY,
        SSO,
        Stefanowski,
        SUNDO,
        Supervised_SMOTE,
        SVM_balance,
        SYMPROD,
        TRIM_SMOTE,
        V_SYNTH,
        VIS_RST,
        MSMOTE,
        SMOTE_AMSR,
        SMOTEWB,
    ]

    return determine_n_quickest(oversamplers, n_quickest)


def get_proportion_oversamplers(n_quickest=-1):
    """
    Returns all oversamplers having a proportion parameter to control the number
    of samples to be generated.

    Args:
        n_quickest (int): the number of quickest oversamplers to return

    Returns:
        list(OverSampling): the list of oversampling classes having a proportion
                            parameter
    """

    oversamplers = get_all_oversamplers()

    oversamplers = [o for o in oversamplers if "proportion" in o().get_params()]

    return determine_n_quickest(oversamplers, n_quickest)


def get_metric_learning_oversamplers(n_quickest=-1):
    """
    Returns all oversampling classes supporting the use of metric learning
    for neighborhood calculations

    Args:
        n_quickest (int): the number of quickest oversamplers to return

    Returns:
        list(OverSampling): the list of all oversampling classes supporting
                            classifier induced distance

    """
    oversamplers = get_all_oversamplers()

    oversamplers = [
        o for o in oversamplers if OverSampling.cat_metric_learning in o.categories
    ]

    return determine_n_quickest(oversamplers, n_quickest)


def check_within_simplex_sampling(ss_params, within_simplex_sampling):
    """
    Check if the ss_params is conformant with the specification in
    within_simplex_sampling

    Args:
        ss_params (dict): the simplex sampling parameters
        within_simplex_sampling (str/list): the specification to check against

    Returns:
        bool: whether the specification is met
    """
    if isinstance(within_simplex_sampling, str):
        return ss_params["within_simplex_sampling"] == within_simplex_sampling

    return ss_params["within_simplex_sampling"] in within_simplex_sampling


def check_gaussian_component(ss_params, with_gaussian_component):
    """
    Check if the ss_params is conformant with the specification of
    with_gaussian_component

    Args:
        ss_params (dict): the simplex sampling parameters
        with_gaussian_component (bool): the specification to check against

    Returns:
        bool: whether the specification is met
    """
    if with_gaussian_component is True:
        return (ss_params["gaussian_component"] is not None) and (
            len(ss_params["gaussian_component"]) > 0
        )
    return not (
        (ss_params["gaussian_component"] is not None)
        and (len(ss_params["gaussian_component"]) > 0)
    )


def check_n_dim(ss_params, n_dim_range):
    """
    Check if the ss_params is conformant with the specification of
    n_dim_range

    Args:
        ss_params (dict): the simplex sampling parameters
        n_dim_range (int/list): the specification to check against

    Returns:
        bool: whether the specification is met
    """
    if isinstance(n_dim_range, int):
        return ss_params["n_dim"] == n_dim_range

    return ss_params["n_dim"] in n_dim_range


def get_simplex_sampling_oversamplers(
    within_simplex_sampling=None,
    exclude_within_simplex_sampling=None,
    with_gaussian_component=None,
    n_dim_range=None,
    n_quickest=-1,
):
    """
    Returns all oversampling classes supporting simplex sampling.

    Args:
        within_simplex_sampling (str/list/None): the value or values for the
                                            within simplex sampling strategies
        within_simplex_sampling (str/list/None): the value or values for the
                                            undesired within simplex sampling
                                            strategies
        with gaussian component (bool): only those techniques not having a
                                        Guassian component set by default
        n_dim_range (int/list): the desired n_dim value(s)
        n_quickest (int): the number of quickest oversamplers to return

    Returns:
        list(OverSampling): the list of all oversampling classes supporting
                            simplex sampling
    """

    oversamplers = get_all_oversamplers()

    results = []

    for os_class in oversamplers:
        if "ss_params" not in list(inspect.signature(os_class).parameters.keys()):
            continue

        ss_params = None
        os_obj = os_class()

        if hasattr(os_obj, "ss_params"):
            ss_params = os_obj.ss_params
        else:
            ss_params = SimplexSamplingMixin.get_params(os_obj)["ss_params"]

        checks = []

        if within_simplex_sampling is not None:
            checks.append(
                check_within_simplex_sampling(ss_params, within_simplex_sampling)
            )

        if exclude_within_simplex_sampling is not None:
            checks.append(
                not check_within_simplex_sampling(
                    ss_params, exclude_within_simplex_sampling
                )
            )

        if with_gaussian_component is not None:
            checks.append(check_gaussian_component(ss_params, with_gaussian_component))

        if n_dim_range is not None:
            checks.append(check_n_dim(ss_params, n_dim_range))

        if len(checks) == 0 or all(checks):
            results.append(os_class)

    return determine_n_quickest(results, n_quickest)


def multiclass_filter(sampler):
    """
    Multiclass filter

    Args:
        sampler (obj): oversampler class

    Returns:
        bool: True if suitable for multiclass oversampling
    """
    term_0 = OverSampling.cat_changes_majority not in sampler.categories
    term_1 = OverSampling.cat_dim_reduction not in sampler.categories
    term_2 = "proportion" in sampler().get_params()

    return term_0 and term_1 and term_2


def get_multiclass_oversamplers(strategy="eq_1_vs_many_successive", n_quickest=-1):
    """
    Returns all oversampling classes which can be used with the multiclass
    strategy specified

    Args:
        strategy (str): the multiclass oversampling strategy -
                        'eq_1_vs_many_successive'/'equalize_1_vs_many'
        n_quickest (int): the number of quickest oversamplers to return

    Returns:
        list(OverSampling): list of all oversampling classes which can be used
                            with the multiclass strategy specified

    Example::

        import smote_variants as sv

        oversamplers= sv.get_multiclass_oversamplers()

    """

    oversamplers = get_all_oversamplers()

    if strategy in ("eq_1_vs_many_successive", "equalize_1_vs_many"):
        oversamplers = [o for o in oversamplers if multiclass_filter(o)]

    return determine_n_quickest(oversamplers, n_quickest)


def get_all_noisefilters():
    """
    Returns all noise filters
    Returns:
        list(NoiseFilter): list of all noise filter classes
    """
    return NoiseFilter.__subclasses__()


def generate_parameter_combinations(
    classes, n_max_comb=5, result_format="smote_variants", random_seed=5
):
    """
    Return random parameter combinations for a set of oversamplers.

    Args:
        list(class): list of oversampling classes
        max_n_comb (int): the maximum number of parameter combinations
                            per oversampling technique
        result_format (str): 'dict'/'smote_variants' - the format of the results
        random_seed (int): the random seed to use
    """
    random_state = np.random.RandomState(random_seed)

    results = []

    for oversampler in classes:
        parameter_combinations = oversampler.parameter_combinations()
        random_state.shuffle(parameter_combinations)
        n_comb = np.min([len(parameter_combinations), n_max_comb])
        parameter_combinations = parameter_combinations[:n_comb]
        parameter_combinations = [
            oversampler(**pcomb).get_params() for pcomb in parameter_combinations
        ]
        results.extend(parameter_combinations)

    if result_format == "dict":
        return results

    return [("smote_variants", r["class_name"], r) for r in results]
