from itertools import izip
import numpy as np
from collections import namedtuple
from scipy.stats import norm
from sklearn import metrics

CalculatedMetric = namedtuple('CalculatedMetric', ['name', 'value'])


class BaseMetricCalculator(object):
    def _one_fold_calculate(self, y_true, y_score):
        raise NotImplementedError()

    def _folds_agg(self, folds_metrics):
        raise NotImplementedError()

    @property
    def _base_name(self):
        raise NotImplementedError()

    def _make_name(self, is_cs):
        if not is_cs:
            return self._base_name
        return self._base_name + '_cs'

    def calculate(self, y_true_folds, y_score_folds, is_cs):
        folds_metrics = []
        for y_true, y_score in izip(y_true_folds, y_score_folds):
            folds_metrics.append(self._one_fold_calculate(y_true, y_score))

        return CalculatedMetric(
            name=self._make_name(is_cs=is_cs),
            value=self._folds_agg(folds_metrics)
        )


class RocAucCalculator(BaseMetricCalculator):
    def _one_fold_calculate(self, y_true, y_score):
        return metrics.roc_auc_score(y_true, y_score)


class MeanAucCalculator(RocAucCalculator):
    @property
    def _base_name(self):
        return 'auc_mean'

    def _folds_agg(self, folds_metrics):
        return np.mean(folds_metrics)


class StdAucCalculator(RocAucCalculator):
    @property
    def _base_name(self):
        return 'auc_std'

    def _folds_agg(self, folds_metrics):
        return np.std(folds_metrics)


class IvCalculator(BaseMetricCalculator):
    def _one_fold_calculate(self, y_true, y_score):
        auc = metrics.roc_auc_score(y_true, y_score)
        return norm.ppf(auc) ** 2 * 2


class MeanIvCalculator(IvCalculator):
    @property
    def _base_name(self):
        return 'iv_mean'

    def _folds_agg(self, folds_metrics):
        return np.mean(folds_metrics)


class StdIvCalculator(IvCalculator):
    @property
    def _base_name(self):
        return 'iv_std'

    def _folds_agg(self, folds_metrics):
        return np.std(folds_metrics)


class R2Calculator(BaseMetricCalculator):
    def _one_fold_calculate(self, y_true, y_score):
        return metrics.r2_score(y_true, y_score)


class MeanR2Calculator(R2Calculator):
    @property
    def _base_name(self):
        return 'r2_mean'

    def _folds_agg(self, folds_metrics):
        return np.mean(folds_metrics)


class StdR2Calculator(R2Calculator):
    @property
    def _base_name(self):
        return 'r2_std'

    def _folds_agg(self, folds_metrics):
        return np.std(folds_metrics)


class MSLECalculator(BaseMetricCalculator):
    def _one_fold_calculate(self, y_true, y_score):
        return metrics.mean_squared_log_error(y_true, y_score)


class MeanMSLECalculator(MSLECalculator):
    @property
    def _base_name(self):
        return 'msle_mean'

    def _folds_agg(self, folds_metrics):
        return np.mean(folds_metrics)


class StdMSLECalculator(MSLECalculator):
    @property
    def _base_name(self):
        return 'msle_std'

    def _folds_agg(self, folds_metrics):
        return np.std(folds_metrics)


class MSECalculator(BaseMetricCalculator):
    def _one_fold_calculate(self, y_true, y_score):
        return metrics.mean_squared_error(y_true, y_score)


class MeanMSECalculator(MSECalculator):
    @property
    def _base_name(self):
        return 'mse_mean'

    def _folds_agg(self, folds_metrics):
        return np.mean(folds_metrics)


class StdMSECalculator(MSECalculator):
    @property
    def _base_name(self):
        return 'mse_std'

    def _folds_agg(self, folds_metrics):
        return np.std(folds_metrics)


name2metric = {
    'auc_mean': MeanAucCalculator,
    'auc_std': StdAucCalculator,
    'iv_mean': MeanIvCalculator,
    'iv_std': StdIvCalculator,
    'r2_mean': MeanR2Calculator,
    'r2_std': StdR2Calculator,
    'msle_mean': MeanMSLECalculator,
    'msle_std': StdMSLECalculator,
    'mse_mean': MeanMSECalculator,
    'mse_std': StdMSECalculator
}

default_metrics = ['auc_mean', 'auc_std', 'iv_mean', 'iv_std']
