# from ...common import requires as _requires

import itertools as _it

from libpysal.weights import W


# I would like to define it like this, so that you could make a call like:
# Geary(df, 'HOVAL', 'INC', w=W), but this only works in Python3. So, I have to
# use a workaround
# def _statistic(df, *cols, stat=None, w=None, inplace=True,
def _univariate_handler(
    df,
    cols,
    stat=None,
    w=None,
    inplace=True,
    pvalue="sim",
    outvals=None,
    swapname="",
    **kwargs,
):
    """
    Compute a univariate descriptive statistic `stat` over columns `cols` in
    `df`.

    Parameters
    ----------
    df          : pandas.DataFrame
                  the dataframe containing columns to compute the descriptive
                  statistics
    cols        : string or list of strings
                  one or more names of columns in `df` to use to compute
                  exploratory descriptive statistics.
    stat        : callable
                  a function that takes data as a first argument and any number
                  of configuration keyword arguments and returns an object
                  encapsulating the exploratory statistic results
    w           : pysal.weights.W
                  the spatial weights object corresponding to the dataframe
    inplace     : bool
                  a flag denoting whether to add the statistic to the dataframe
                  in memory, or to construct a copy of the dataframe and append
                  the results to the copy
    pvalue      : string
                  the name of the pvalue on the results object wanted
    outvals     : list of strings
                  names of attributes of the dataframe to attempt to flatten
                  into a column
    swapname    : string
                  suffix to replace generic identifier with. Each caller of this
                  function should set this to a unique column suffix
    **kwargs    : optional keyword arguments
                  options that are passed directly to the statistic
    """
    # Preprocess
    if not inplace:
        new_df = df.copy()
        _univariate_handler(
            new_df,
            cols,
            stat=stat,
            w=w,
            pvalue=pvalue,
            inplace=True,
            outvals=outvals,
            swapname=swapname,
            **kwargs,
        )
        return new_df
    if w is None:
        for name in df._metadata:
            this_obj = df.__dict__.get(name)
            if isinstance(this_obj, W):
                w = this_obj
    if w is None:
        raise Exception(
            "Weights not provided and no weights attached to frame! "
            "Please provide a weight or attach a weight to the dataframe."
        )
    # Prep indexes
    if outvals is None:
        outvals = []
    outvals.insert(0, "_statistic")
    if pvalue.lower() in ["all", "both", "*"]:
        raise NotImplementedError(
            "If you want more than one type of PValue, add the "
            "targeted pvalue type to outvals. For example:\n"
            "\tGeary(df, cols=['HOVAL'], w=w, outvals=['p_z_sim', 'p_rand']"
        )
    # this is nontrivial, since we
    # can't know which p_value types are on the object without computing it.
    # This is because we don't flag them with @properties, so they're just
    # arbitrarily assigned post-facto. One solution might be to post-process the
    # objects, determine which pvalue types are available, and then grab them
    # all if needed.

    if pvalue != "":
        outvals.append("p_" + pvalue.lower())
    if isinstance(cols, str):
        cols = [cols]

    # Make closure around weights & apply columnwise
    def column_stat(column):
        return stat(column.values, w=w, **kwargs)

    stat_objs = df[cols].apply(column_stat)

    # Assign into dataframe
    for col in cols:
        stat_obj = stat_objs[col]
        y = kwargs.get("y")
        if y is not None:
            col += "-" + y.name
        outcols = ["_".join((col, val)) for val in outvals]
        for colname, attname in zip(outcols, outvals, strict=True):
            df[colname] = stat_obj.__getattribute__(attname)
    if swapname != "":
        df.columns = [
            _swap_ending(col, swapname) if col.endswith("_statistic") else col
            for col in df.columns
        ]


def _bivariate_handler(
    df, x, y=None, w=None, inplace=True, pvalue="sim", outvals=None, **kwargs
):
    """
    Compute a descriptive bivariate statistic over two sets of columns, `x` and
    `y`, contained in `df`.

    Parameters
    ----------
    df          : pandas.DataFrame
                  dataframe in which columns `x` and `y` are contained
    x           : string or list of strings
                  one or more column names to use as variates in the bivariate
                  statistics
    y           : string or list of strings
                  one or more column names to use as variates in the bivariate
                  statistics
    w           : pysal.weights.W
                  spatial weights object corresponding to the dataframe `df`
    inplace     : bool
                  a flag denoting whether to add the statistic to the dataframe
                  in memory, or to construct a copy of the dataframe and append
                  the results to the copy
    pvalue      : string
                  the name of the pvalue on the results object wanted
    outvals     : list of strings
                  names of attributes of the dataframe to attempt to flatten
                  into a column
    swapname    : string
                  suffix to replace generic identifier with. Each caller of this
                  function should set this to a unique column suffix
    **kwargs    : optional keyword arguments
                  options that are passed directly to the statistic
    """
    real_swapname = kwargs.pop("swapname", "")
    if isinstance(y, str):
        y = [y]
    if isinstance(x, str):
        x = [x]
    if not inplace:
        new_df = df.copy()
        _bivariate_handler(
            new_df,
            x,
            y=y,
            w=w,
            inplace=True,
            swapname=real_swapname,
            pvalue=pvalue,
            outvals=outvals,
            **kwargs,
        )
        return new_df
    if y is None:
        y = x
    for xi, yi in _it.product(x, y):
        if xi == yi:
            continue
        _univariate_handler(
            df,
            cols=xi,
            w=w,
            y=df[yi],
            inplace=True,
            pvalue=pvalue,
            outvals=outvals,
            swapname="",
            **kwargs,
        )
    if real_swapname != "":
        df.columns = [
            _swap_ending(col, real_swapname) if col.endswith("_statistic") else col
            for col in df.columns
        ]


def _swap_ending(s, ending, delim="_"):
    """
    Replace the ending of a string, delimited into an arbitrary
    number of chunks by `delim`, with the ending provided

    Parameters
    ----------
    s       :   string
                string to replace endings
    ending  :   string
                string used to replace ending of `s`
    delim   :   string
                string that splits s into one or more parts

    Returns
    -------
    new string where the final chunk of `s`, delimited by `delim`, is replaced
    with `ending`.
    """
    parts = [x for x in s.split(delim)[:-1] if x != ""]
    parts.append(ending)
    return delim.join(parts)


##############
# DOCSTRINGS #
##############

_univ_doc_template = """
Function to compute a {n} statistic on a dataframe

Parameters
----------
df          :   pandas.DataFrame
                a pandas dataframe with a geometry column
cols        :   string or list of string
                name or list of names of columns to use to compute the statistic
w           :   pysal weights object
                a weights object aligned with the dataframe. If not provided, this
                is searched for in the dataframe's metadata
inplace     :   bool
                a boolean denoting whether to operate on the dataframe inplace or to
                return a series contaning the results of the computation. If
                operating inplace, the derived columns will be named 'column_{nl}'
pvalue      :   string
                a string denoting which pvalue should be returned. Refer to the
                the {n} statistic's documentation for available p-values
outvals     :   list of strings
                list of arbitrary attributes to return as columns from the
                {n} statistic
**stat_kws  :   keyword arguments
                options to pass to the underlying statistic. For this, see the
                documentation for the {n} statistic.

Returns
--------
If inplace, None, and operation is conducted on dataframe in memory. Otherwise,
returns a copy of the dataframe with the relevant columns attached.

See Also
---------
For further documentation, refer to the {n} class in pysal.esda
"""

_bv_doc_template = """
Function to compute a {n} statistic on a dataframe

Parameters
----------
df          :   pandas.DataFrame
                a pandas dataframe with a geometry column
X           :   list of strings
                column name or list of column names to use as X values to compute
                the bivariate statistic. If no Y is provided, pairwise comparisons
                among these variates are used instead.
Y           :   list of strings
                column name or list of column names to use as Y values to compute
                the bivariate statistic. if no Y is provided, pariwise comparisons
                among the X variates are used instead.

w           :   pysal weights object
                a weights object aligned with the dataframe. If not provided, this
                is searched for in the dataframe's metadata
inplace     :   bool
                a boolean denoting whether to operate on the dataframe inplace or to
                return a series contaning the results of the computation. If
                operating inplace, the derived columns will be named 'column_{nl}'
pvalue      :   string
                a string denoting which pvalue should be returned. Refer to the
                the {n} statistic's documentation for available p-values
outvals     :   list of strings
                list of arbitrary attributes to return as columns from the
                {n} statistic
**stat_kws  :   keyword arguments
                options to pass to the underlying statistic. For this, see the
                documentation for the {n} statistic.


Returns
--------
If inplace, None, and operation is conducted on dataframe in memory. Otherwise,
returns a copy of the dataframe with the relevant columns attached.

See Also
---------
For further documentation, refer to the {n} class in pysal.esda
"""

_rate_doc_template = """
Function to compute a {n} statistic on a dataframe

Parameters
----------
df          :   pandas.DataFrame
                a pandas dataframe with a geometry column
events      :   string or  list of strings
                one or more names where events are stored
populations :   string or list of strings
                one or more names where the populations corresponding to the
                events are stored. If one population column is provided, it is
                used for all event columns. If more than one population column
                is provided but there is not a population for every event
                column, an exception will be raised.
w           :   pysal weights object
                a weights object aligned with the dataframe. If not provided, this
                is searched for in the dataframe's metadata
inplace     :   bool
                a boolean denoting whether to operate on the dataframe inplace or to
                return a series contaning the results of the computation. If
                operating inplace, the derived columns will be named 'column_{nl}'
pvalue      :   string
                a string denoting which pvalue should be returned. Refer to the
                the {n} statistic's documentation for available p-values
outvals     :   list of strings
                list of arbitrary attributes to return as columns from the
                {n} statistic
**stat_kws  :   keyword arguments
                options to pass to the underlying statistic. For this, see the
                documentation for the {n} statistic.

Returns
--------
If inplace, None, and operation is conducted on dataframe in memory. Otherwise,
returns a copy of the dataframe with the relevant columns attached.

See Also
---------
For further documentation, refer to the {n} class in pysal.esda
"""
