import numpy as np
import scipy.stats as stats
import scipy.special as special
import scipy.spatial.distance as distance
from itertools import combinations
from sklearn.utils import check_array
from .base import BaseThresholder
from .thresh_utility import normalize, cut, gen_kde

def JS_metric(val_data, val_norm):
    """Calculate the Jensen-Shannon distance"""

    return 1-distance.jensenshannon(val_data, val_norm)

def WS_metric(val_data, val_norm):
    """Calculate the Wasserstein or Earth Movers distance"""

    return stats.wasserstein_distance(val_data, val_norm)

def ENG_metric(val_data, val_norm):
    """Calculate the Energy distance"""

    return stats.energy_distance(val_data, val_norm)

def BHT_metric(val_data, val_norm):
    """Calculate the Bhattacharyya distance"""

    val_data = val_data/np.sum(val_data)
    val_norm = val_norm/np.sum(val_norm)

    bht = np.sum(np.sqrt(val_data*val_norm))
    return np.log1p(bht)

def HLL_metric(val_data, val_norm):
    """Calculate the Hellinger distance"""

    val_data = val_data/np.sum(val_data)
    val_norm = val_norm/np.sum(val_norm)

    return (1-distance.euclidean(np.sqrt(val_data), np.sqrt(val_norm))
                     /np.sqrt(2))

def HI_metric(val_data, val_norm):
    """Calculate the Histogram intersection distance"""

    val_data = val_data/np.sum(val_data)
    val_norm = val_norm/np.sum(val_norm)
    
    return np.sum(np.minimum(val_data,val_norm))

def LK_metric(val_data, val_norm):
    """Calculate the Lukaszyk–Karmowski metric for normal distributions"""
            
    # Get expected values for both distributions
    rng = np.linspace(-1,1,len(val_data))
    exp_data = (rng*val_data).sum()/val_data.sum()
    exp_norm = (rng*val_norm).sum()/val_norm.sum()
            
    nu_xy = np.abs(exp_data-exp_norm)

    # STD is same for both distributions
    std = np.std(rng)

    # Get the 1-LK distance
    return 1-(nu_xy + 2*std/np.sqrt(np.pi)*np.exp(-nu_xy**2/(4*std**2))
                     - nu_xy*special.erfc(nu_xy/(2*std)))

def LP_metric(decision, norm):
    """Calculate the Levy-Prokhorov metric"""

    # Get the edges for the complete graphs of the datasets
    f1 = np.array(list(combinations(decision.tolist(),2)))
    f2 = np.array(list(combinations(norm.tolist(),2)))

    return distance.directed_hausdorff(f1,f2)[0]

def MAH_metric(val_data, val_norm):
    """Calculate the Mahalanobis distance"""

    val_data = val_data/np.sum(val_data)
    val_norm = val_norm/np.sum(val_norm)

    V = np.cov(np.array([val_data,val_norm]).T)
    IV = np.linalg.inv(V)
    return distance.mahalanobis(val_data,val_norm,IV)/len(val_data)

def TMT_metric(val_data, val_norm):
    """Calculate the Tanimoto distance"""
    
    p = np.sum(val_data)
    q = np.sum(val_norm)
    m = np.sum(np.minimum(val_data,val_norm))

    return 1-(p+q-2*m)/(p+q-m)

def RES_metric(val_data, val_norm):
    """Calculate the studentized residual distance"""

    mean_X = np.mean(val_data)
    mean_Y = np.mean(val_norm)
    n = len(val_data)
    
    diff_mean_sqr = np.dot((val_data - mean_X), (val_data - mean_X))
    beta1 = np.dot((val_data - mean_X), (val_norm - mean_Y)) / diff_mean_sqr
    beta0 = mean_Y - beta1 * mean_X
    
    y_hat = beta0 + beta1 * val_data
    residuals = val_norm - y_hat
    
    h_ii = (val_data - mean_X) ** 2 / diff_mean_sqr + (1 / n)
    Var_e = np.sqrt(np.sum((val_norm - y_hat) ** 2)/(n-2))
    
    SE_regression = Var_e*((1-h_ii) ** 0.5)
    studentized_residuals = residuals/SE_regression
    
    return np.abs(np.sum(studentized_residuals))


class DSN(BaseThresholder):
    """DSN class for Distance Shift from Normal thresholder.

       Use the distance shift from normal to evaluate a non-parametric means
       to threshold scores generated by the decision_scores where outliers
       are set to any value beyond the distance calculated by the selected
       metric
       
       Paramaters
       ----------

       metric : str, optional (default='LK')
        {'JS', 'WS', 'ENG', 'BHT', 'HLL', 'HI', 'LK', 'LP', 'MAH', 'TMT', 'RES'}
        'JS':  Jensen-Shannon distance
        'WS':  Wasserstein or Earth Movers distance
        'ENG': Energy distance
        'BHT': Bhattacharyya distance
        'HLL': Hellinger distance
        'HI':  Histogram intersection distance
        'LK':  Lukaszyk–Karmowski metric for normal distributions
        'LP':  Levy-Prokhorov metric
        'MAH': Mahalanobis distance
        'TMT': Tanimoto distance
        'RES': Studentized residual distance

       Attributes
       ----------

       eval_: numpy array of binary labels of the training data. 0 stands
           for inliers and 1 for outliers/anomalies.

    """

    def __init__(self, metric='LK'):

        super(DSN, self).__init__()
        self.metric = metric
        self.metric_funcs = {'JS': JS_metric, 'WS': WS_metric,
                             'ENG': ENG_metric, 'BHT': BHT_metric,
                             'HLL': HLL_metric, 'HI': HI_metric,
                             'LK':LK_metric, 'LP': LP_metric,
                             'MAH': MAH_metric, 'TMT': TMT_metric,
                             'RES':RES_metric}

    def eval(self, decision):
        """Outlier/inlier evaluation process for decision scores.

        Parameters
        ----------
        decision : np.array or list of shape (n_samples)
                   which are the decision scores from a
                   outlier detection.
        
        Returns
        -------
        outlier_labels : numpy array of shape (n_samples,)
            For each observation, tells whether or not
            it should be considered as an outlier according to the
            fitted model. 0 stands for inliers and 1 for outliers.
        """

        decision = check_array(decision, ensure_2d=False)

        decision = normalize(decision)

        #Create a normal distribution and normalize
        norm = np.random.normal(loc=0.0, scale=1.0, size=decision.shape)
        norm = normalize(norm)

        if (self.metric!='LP') or (self.metric!='RES'):
            # Create a KDE of the decision scores and the normal distribution
            # Generate KDE
            val_data, _ = gen_kde(decision,-1,1,len(decision)*3)
            val_norm, _ = gen_kde(norm,-1,1,len(decision)*3)

            val_data = val_data/np.max(val_data)
            val_norm = val_norm/np.max(val_norm)

        
            limit = self.metric_funcs[str(self.metric)](val_data, val_norm)
            
        else:
            limit = self.metric_funcs[str(self.metric)](np.sort(decision), norm)

        self.thresh_ = limit

        return cut(decision, limit)
