import numpy as np
import scipy.stats.mstats as scistats
import sklearn.metrics as scimet
import sklearn.preprocessing as skpp
from fastnet.log import logger


def sp(truePositive, trueNegative):
    """function ret  = calcSP(sp(truePositive, trueNegative))
    Calculates the normalized [0,1] SP value.
        Input Parameters are:
            truePositive -> The detection efficiency as an scalar or a (N,) array.
            trueNegative -> The true negative (1 - false alarm) as an scalar or a (N,) array.

    THIS FUNCTION WORKS ONLY FOR A 2-CLASS CLASSIFIER.
    """
    m = np.concatenate((np.atleast_2d(truePositive), np.atleast_2d(trueNegative)))
    ret = np.sqrt(scistats.gmean(m, axis = 0) * np.mean(m, axis = 0))
    return ret[0] if ret.shape[0] == 1 else ret


def roc(outSignal, outNoise, numPts = 1000, algo = 'scikit'):
    """function [det, fa, sp, ppv, ppn, acu, thres] = roc(outSignal, outNoise, numPts = 1000, algo = 'scikit')
    Returns the Receiver Operational Characterists.
    Input Parameters are:
        outSignal                  -> The output (as (N,) numpy vector) generated by your classifier when signal-related data is applied to it.
        outNoise                   -> The output (as (N,) numpy vector) generated by your classifier when noise-related data is applied to it.
        numPts                     -> How many points the ROC should have (defaults to 1000). Ignored if algo != 'matlab'.
        algo ('matlab', 'scikit')  -> Can be either the Matlab algo or use the SciKit-Learn roc function (default).
    Returns:
        det   -> Detection efficiency (true positive probability) per decision threshold as a numpy (N,) vector).
        fa    -> False alarm efficiency (false positive probability) per decision threshold as a numpy (N,) vector).
        sp    -> The SP value per decision threshold as a numpy (N,) vector).
        ppv   -> The Positive Predicted Value per decision threshold as a numpy (N,) vector).
        npv   -> The Negative Predicted Value per decision threshold as a numpy (N,) vector).
        acu   -> Acuity per decision threshold as a numpy (N,) vector).
        thres -> The decision threshold as a numpy (N,) vector)

    THIS FUNCTION WORKS ONLY FOR A 2-CLASS CLASSIFIER.
    """

    def matlab_roc(outSignal, outNoise, numPts):
        fullData = np.concatenate((outSignal, outNoise))
        doNorm = (fullData.min() < -1) or (fullData.max() > 1)
        if doNorm:
            logger.info('Normalizing output values so they can be within the [-1,1] range.')
            pp = skpp.MinMaxScaler(feature_range=(-1, 1))
            pp.fit(fullData.reshape(-1,1))
            outSignal = pp.transform(outSignal.reshape(-1,1)).flatten()
            outNoise = pp.transform(outNoise.reshape(-1,1)).flatten()
        cutVec = np.linspace(-1,1,numPts)
        det = np.zeros(numPts)
        fa = np.zeros(numPts)
        for i, cut in enumerate(cutVec):
            det[i], fa[i] = efficiency(outSignal, outNoise, cut)[0:2]
        if doNorm: cutVec = pp.inverse_transform(cutVec.reshape(-1,1)).flatten()
        return fa, det, cutVec

    def sci_roc(outSignal, outNoise, *kwargs):
        #Converting to scipy format (output and target)
        output = np.concatenate((outSignal, outNoise))
        target = np.concatenate( (np.ones(outSignal.shape), -np.ones(outNoise.shape)) )
        fa, det, thres = scimet.roc_curve(target, output, pos_label = 1)
        return fa, det, thres

    assert algo in ['scikit', 'matlab'] , 'Algoritmo de cálculod e ROC inválido!'
    roc_algo = matlab_roc if algo == 'matlab' else sci_roc
    fa, det, thres = roc_algo(outSignal, outNoise, numPts)

    nSignal = len(outSignal)
    nNoise = len(outNoise)
    tn = 1-fa
    fn = 1-det
    vpp = det / (det + fa)
    vpn = tn / (tn + fn)
    acc = (det*nSignal + tn*nNoise) / (nSignal + nNoise)
    spVec = sp(det, tn)

    return det, fa, spVec, vpp, vpn, acc, thres



def efficiency(outSignal, outNoise, cut = 0.):
    """function [det, fa, sp, ppv, ppn, acu] = efficiency(outSignal, outNoise, cut)
    Returns the efficiency parameters for a detecor.
    Input Parameters are:
        outSignal -> The output (as (N,) numpy vector) generated by your classifier when signal-related data is applied to it.
        outNoise  -> The output (as (N,) numpy vector) generated by your classifier when noise-related data is applied to it.
        cut       -> The decision threshold (defaults to 0). The function considers as signal any output >= cut.
    Returns:
        det -> Detection efficiency (true positive probability)
        fa  -> False alarm efficiency (false positive probability)
        sp  -> The SP value
        ppv -> The Positive Predicted Value
        npv -> The Negative Predicted Value
        acu -> Acuity

    THIS FUNCTION WORKS ONLY FOR A 2-CLASS CLASSIFIER.
    """

    nSignal = len(outSignal)
    nNoise = len(outNoise)
    det = np.sum(outSignal >= cut) / nSignal
    fa = np.sum(outNoise >= cut) / nNoise
    tn = 1-fa
    fn = 1-det
    acu = (det*nSignal + tn*nNoise) / (nSignal + nNoise)
    vpp = det / (det + fa)
    vpn = tn / (tn + fn)
    spVal = sp(det, tn)
    return det, fa, spVal, vpp, vpn, acu
