"""helper functions conversion between moments

contains:

* conversion between central and non-central moments, skew, kurtosis and
  cummulants
* cov2corr : convert covariance matrix to correlation matrix


Author: Josef Perktold
License: BSD-3

"""

import numpy as np
from scipy.special import comb


def _convert_to_multidim(x):
    if any([isinstance(x, list), isinstance(x, tuple)]):
        return np.array(x)
    elif isinstance(x, np.ndarray):
        return x
    else:
        # something strange was passed and the function probably
        # will fall, maybe insert an exception?
        return x


def _convert_from_multidim(x, totype=list):
    if len(x.shape) < 2:
        return totype(x)
    return x.T


def mc2mnc(mc):
    """convert central to non-central moments, uses recursive formula
    optionally adjusts first moment to return mean
    """
    x = _convert_to_multidim(mc)

    def _local_counts(mc):
        mean = mc[0]
        mc = [1] + list(mc)  # add zero moment = 1
        mc[1] = 0  # define central mean as zero for formula
        mnc = [1, mean]  # zero and first raw moments
        for nn, _ in enumerate(mc[2:]):
            n = nn + 2
            mnc.append(0)
            for k in range(n + 1):
                mnc[n] += comb(n, k, exact=True) * mc[k] * mean ** (n - k)
        return mnc[1:]

    res = np.apply_along_axis(_local_counts, 0, x)
    # for backward compatibility convert 1-dim output to list/tuple
    return _convert_from_multidim(res)


def mnc2mc(mnc, wmean=True):
    """convert non-central to central moments, uses recursive formula
    optionally adjusts first moment to return mean
    """
    X = _convert_to_multidim(mnc)

    def _local_counts(mnc):
        mean = mnc[0]
        mnc = [1] + list(mnc)  # add zero moment = 1
        mu = []
        for n, _ in enumerate(mnc):
            mu.append(0)
            for k in range(n + 1):
                sgn_comb = (-1) ** (n - k) * comb(n, k, exact=True)
                mu[n] += sgn_comb * mnc[k] * mean ** (n - k)
        if wmean:
            mu[1] = mean
        return mu[1:]

    res = np.apply_along_axis(_local_counts, 0, X)
    # for backward compatibility convert 1-dim output to list/tuple
    return _convert_from_multidim(res)


def cum2mc(kappa):
    """convert non-central moments to cumulants
    recursive formula produces as many cumulants as moments

    References
    ----------
    Kenneth Lange: Numerical Analysis for Statisticians, page 40
    """
    X = _convert_to_multidim(kappa)

    def _local_counts(kappa):
        mc = [1, 0.0]  # _kappa[0]]  # insert 0-moment and mean
        kappa0 = kappa[0]
        kappa = [1] + list(kappa)
        for nn, _ in enumerate(kappa[2:]):
            n = nn + 2
            mc.append(0)
            for k in range(n - 1):
                mc[n] += comb(n - 1, k, exact=True) * kappa[n - k] * mc[k]
        mc[1] = kappa0  # insert mean as first moments by convention
        return mc[1:]

    res = np.apply_along_axis(_local_counts, 0, X)
    # for backward compatibility convert 1-dim output to list/tuple
    return _convert_from_multidim(res)


def mnc2cum(mnc):
    """convert non-central moments to cumulants
    recursive formula produces as many cumulants as moments

    https://en.wikipedia.org/wiki/Cumulant#Cumulants_and_moments
    """
    X = _convert_to_multidim(mnc)

    def _local_counts(mnc):
        mnc = [1] + list(mnc)
        kappa = [1]
        for nn, m in enumerate(mnc[1:]):
            n = nn + 1
            kappa.append(m)
            for k in range(1, n):
                num_ways = comb(n - 1, k - 1, exact=True)
                kappa[n] -= num_ways * kappa[k] * mnc[n - k]
        return kappa[1:]

    res = np.apply_along_axis(_local_counts, 0, X)
    # for backward compatibility convert 1-dim output to list/tuple
    return _convert_from_multidim(res)


def mc2cum(mc):
    """
    just chained because I have still the test case
    """
    first_step = mc2mnc(mc)
    if isinstance(first_step, np.ndarray):
        first_step = first_step.T
    return mnc2cum(first_step)
    # return np.apply_along_axis(lambda x: mnc2cum(mc2mnc(x)), 0, mc)


def mvsk2mc(args):
    """convert mean, variance, skew, kurtosis to central moments"""
    X = _convert_to_multidim(args)

    def _local_counts(args):
        mu, sig2, sk, kur = args
        cnt = [None] * 4
        cnt[0] = mu
        cnt[1] = sig2
        cnt[2] = sk * sig2 ** 1.5
        cnt[3] = (kur + 3.0) * sig2 ** 2.0
        return tuple(cnt)

    res = np.apply_along_axis(_local_counts, 0, X)
    # for backward compatibility convert 1-dim output to list/tuple
    return _convert_from_multidim(res, tuple)


def mvsk2mnc(args):
    """convert mean, variance, skew, kurtosis to non-central moments"""
    X = _convert_to_multidim(args)

    def _local_counts(args):
        mc, mc2, skew, kurt = args
        mnc = mc
        mnc2 = mc2 + mc * mc
        mc3 = skew * (mc2 ** 1.5)  # 3rd central moment
        mnc3 = mc3 + 3 * mc * mc2 + mc ** 3  # 3rd non-central moment
        mc4 = (kurt + 3.0) * (mc2 ** 2.0)  # 4th central moment
        mnc4 = mc4 + 4 * mc * mc3 + 6 * mc * mc * mc2 + mc ** 4
        return (mnc, mnc2, mnc3, mnc4)

    res = np.apply_along_axis(_local_counts, 0, X)
    # for backward compatibility convert 1-dim output to list/tuple
    return _convert_from_multidim(res, tuple)


def mc2mvsk(args):
    """convert central moments to mean, variance, skew, kurtosis"""
    X = _convert_to_multidim(args)

    def _local_counts(args):
        mc, mc2, mc3, mc4 = args
        skew = np.divide(mc3, mc2 ** 1.5)
        kurt = np.divide(mc4, mc2 ** 2.0) - 3.0
        return (mc, mc2, skew, kurt)

    res = np.apply_along_axis(_local_counts, 0, X)
    # for backward compatibility convert 1-dim output to list/tuple
    return _convert_from_multidim(res, tuple)


def mnc2mvsk(args):
    """convert central moments to mean, variance, skew, kurtosis
    """
    X = _convert_to_multidim(args)

    def _local_counts(args):
        # convert four non-central moments to central moments
        mnc, mnc2, mnc3, mnc4 = args
        mc = mnc
        mc2 = mnc2 - mnc * mnc
        mc3 = mnc3 - (3 * mc * mc2 + mc ** 3)  # 3rd central moment
        mc4 = mnc4 - (4 * mc * mc3 + 6 * mc * mc * mc2 + mc ** 4)
        return mc2mvsk((mc, mc2, mc3, mc4))

    res = np.apply_along_axis(_local_counts, 0, X)
    # for backward compatibility convert 1-dim output to list/tuple
    return _convert_from_multidim(res, tuple)

# def mnc2mc(args):
#    """convert four non-central moments to central moments
#    """
#    mnc, mnc2, mnc3, mnc4 = args
#    mc = mnc
#    mc2 = mnc2 - mnc*mnc
#    mc3 = mnc3 - (3*mc*mc2+mc**3) # 3rd central moment
#    mc4 = mnc4 - (4*mc*mc3+6*mc*mc*mc2+mc**4)
#    return mc, mc2, mc

# TODO: no return, did it get lost in cut-paste?


def cov2corr(cov, return_std=False):
    """
    convert covariance matrix to correlation matrix

    Parameters
    ----------
    cov : array_like, 2d
        covariance matrix, see Notes

    Returns
    -------
    corr : ndarray (subclass)
        correlation matrix
    return_std : bool
        If this is true then the standard deviation is also returned.
        By default only the correlation matrix is returned.

    Notes
    -----
    This function does not convert subclasses of ndarrays. This requires that
    division is defined elementwise. np.ma.array and np.matrix are allowed.
    """
    cov = np.asanyarray(cov)
    std_ = np.sqrt(np.diag(cov))
    corr = cov / np.outer(std_, std_)
    if return_std:
        return corr, std_
    else:
        return corr


def corr2cov(corr, std):
    """
    convert correlation matrix to covariance matrix given standard deviation

    Parameters
    ----------
    corr : array_like, 2d
        correlation matrix, see Notes
    std : array_like, 1d
        standard deviation

    Returns
    -------
    cov : ndarray (subclass)
        covariance matrix

    Notes
    -----
    This function does not convert subclasses of ndarrays. This requires
    that multiplication is defined elementwise. np.ma.array are allowed, but
    not matrices.
    """
    corr = np.asanyarray(corr)
    std_ = np.asanyarray(std)
    cov = corr * np.outer(std_, std_)
    return cov


def se_cov(cov):
    """
    get standard deviation from covariance matrix

    just a shorthand function np.sqrt(np.diag(cov))

    Parameters
    ----------
    cov : array_like, square
        covariance matrix

    Returns
    -------
    std : ndarray
        standard deviation from diagonal of cov
    """
    return np.sqrt(np.diag(cov))
