import numpy as np
import pandas as pd
import os.path
from exceptions import Exception
 
import abc



from sklearn.neighbors import KNeighborsClassifier
from sklearn.base import BaseEstimator, ClassifierMixin
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import MinMaxScaler

from sklearn.cluster import KMeans
from sklearn.cross_validation import StratifiedKFold
from metric_learn import LMNN


import sys
# sys.path.append('..\\..\\pyclf\\model')
sys.path.append('..\\validation')
sys.path.append('..\\pyutils')

from frq_features import FRQFeatures

#sys.path.append(os.path.join(os.path.dirname(__file__), '..\\..\\pyutils'))


import weight


class LocalLMNNClf(BaseEstimator, ClassifierMixin):
    def __init__(self, cluster_idx, 
                       lmnn_k=1, 
                       lmnn_learn_rate=1e-5,
                       lmnn_max_iter=500,
                       lmnn_convergence_tol=0.01,
                       lmnn_regularization=0.5,
                       knn_k=1):

        self.lmnn_k = lmnn_k
        self.lmnn_learn_rate = lmnn_learn_rate
        self.lmnn_max_iter = lmnn_max_iter
        self.lmnn_convergence_tol = lmnn_convergence_tol
        self.lmnn_regularization = lmnn_regularization
        self.knn_k = knn_k

        self.lmnn = LMNN(k=lmnn_k, 
                         learn_rate=lmnn_learn_rate,
                         max_iter=lmnn_max_iter, 
                         convergence_tol=lmnn_convergence_tol,
                         regularization=lmnn_regularization)

        self.knn = KNeighborsClassifier(n_neighbors=knn_k,
                                        weights = 'uniform', 
                                        algorithm = 'brute',
                                        metric = 'minkowski', 
                                        p=2)

        self.clf = Pipeline([ ("LMNN", self.lmnn),
                              ("KNN", self.knn) ])
        self.cluster_idx = cluster_idx
        

    def fit(self, X, y):
        if len(X) != len(y):
            raise ValueError()
        self.X = X
        self.y = y 
        self.classes = list(sorted(set(y)))
        if len(self.classes) > 1:
            self.clf.fit(X , y)


    def predict(self, X):
        if len(self.classes) > 1:
            return self.clf.predict(X)
        else:
            return np.array([self.classes[0]]*len(X))


    def __hash__(self):
        return self.cluster_idx


class KMeansLMNN(BaseEstimator, ClassifierMixin):
    """First find clusters and then trains LMNN for each cluster that has more
       than 1 class
    """

    def __init__(self, n_clusters, 
                       init_w, 
                       kmeans_n_init=10,
                       kmeans_max_iter=1000,
                       lmnn_k=1, 
                       lmnn_learn_rate=1e-5,
                       lmnn_max_iter=500,
                       lmnn_convergence_tol=0.01,
                       lmnn_regularization=0.5,
                       knn_k=1):

        self.n_clusters = n_clusters
        self.init_w = init_w
        self.kmeans_n_init = kmeans_n_init
        self.kmeans_max_iter = kmeans_max_iter
        self.lmnn_k = lmnn_k
        self.lmnn_learn_rate = lmnn_learn_rate
        self.lmnn_max_iter = lmnn_max_iter
        self.lmnn_convergence_tol = lmnn_convergence_tol
        self.lmnn_regularization = lmnn_regularization
        self.knn_k = knn_k     
        

    def fit(self, X, y, print_progress=False):
        print 1
        self.preproc = Pipeline([ ("Normalization", MinMaxScaler()),
                                  ("Assign_Weights", weight.Weight(self.init_w ))
                                 ])
  
        self.kmeans =  KMeans(n_clusters = self.n_clusters, n_init=10, max_iter=1000)

        
        Xt = self.preproc.fit_transform(X)
        self.kmeans.fit(Xt)
        

        cluster_ind = self.kmeans.predict(Xt)

        local_clfs = {}

        for i in range(self.n_clusters):
            ind = cluster_ind==i
            Xtc = Xt[ind]
            ytc = y[ind]
            if print_progress:
                msg = "training " + str(i+1) + "/" + str(self.n_clusters) + " cluster..."
                msg +=  "  classes: " + str(sorted(set(ytc))) 
                msg += " Cluster size:" + str(len(Xtc))
                print msg
            local = LocalLMNNClf(i, lmnn_k=self.lmnn_k,
                                     lmnn_learn_rate=self.lmnn_learn_rate,
                                     lmnn_max_iter=self.lmnn_max_iter, 
                                     lmnn_convergence_tol=self.lmnn_convergence_tol,
                                     lmnn_regularization=self.lmnn_regularization,
                                     knn_k=self.knn_k)
            local.fit(Xtc, ytc)
            local_clfs[i] = local

        self.local_clfs = local_clfs
        
        
    def predict(self, X):
        Xt = self.preproc.transform(X)
        clusters_pred = self.kmeans.predict(Xt)
        y_pred = np.empty((len(X),), dtype='|S200')

        for idx in sorted(set(clusters_pred)):
            Xtc = Xt[clusters_pred == idx]
            y_pred[clusters_pred == idx] = self.local_clfs[idx].predict(Xtc)

        return y_pred
    

class KNNBasedClfABC(BaseEstimator):
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def set_feature_weights(self, weights):
        return self

    @abc.abstractmethod
    def fit(self, X, y):
        return self

    @abc.abstractmethod
    def predict(self, X):
        return np.array([0])


class KNNWeightedFeatures(KNNBasedClfABC):
    def __init__(self, weights):
        self.weights = weights
        self._update_clf()


    def set_feature_weights(self, weights):
        self.weights = weights
        self._update_clf()
        return self


    def _update_clf(self):
        knn = KNeighborsClassifier(n_neighbors=1, weights = 'uniform', 
                                   algorithm = 'brute',
                                   metric = 'minkowski', p=2)

        self.clf = Pipeline([ ("Normalization", MinMaxScaler()),
                              ("Assign_Weights", weight.Weight(self.weights)),
                              ("KNN", knn)])


    def fit(self, X, y):
        self.clf.fit(X, y)
        return self


    def predict(self, X):
        return self.clf.predict(X)

    def kneighbors(self, X, n_neighbors=None, return_distance=True):
        Xt = self.clf.steps[0][1].transform(X)
        Xt = self.clf.steps[1][1].transform(Xt)
        return self.clf.steps[2][1].kneighbors(Xt, n_neighbors, return_distance)


class ClfTestABC:
    __metaclass__ = abc.ABCMeta

    def __init__(self, X, y, score_func):
        """
        score_func takes true labels and predicted labels and
        returns score
        score_func(y_true, y_pred) 

        """
        self.X = X
        self.y = y
        self.score_func = score_func

    @abc.abstractmethod
    def evaluate(self, clf):
        """
        should evaluate a model in some way using training data X, y
        for example cross validate it, or split into 
        training and validation tests
        """
        y_true = np.array([0,1])
        y_pred = np.array([0,1])
        return self.score_func(y_true, y_pred)


class TrainValSplitClfTest(ClfTestABC):
    def __init__(self, X, y, score_func, train_ind, val_ind):
        ClfTestABC.__init__(self, X, y, score_func)
        self.train_ind = train_ind
        self.val_ind = val_ind
        self.Xtrain = self.X[train_ind]
        self.ytrain = self.y[train_ind]
        self.Xval = self.X[val_ind]
        self.yval = self.y[val_ind]

    def evaluate(self, clf):
        clf.fit(self.Xtrain, self.ytrain)
        self.ypred = clf.predict(self.Xval)
        return self.score_func(self.yval, self.ypred)


class CvClfTest(ClfTestABC):
    def __init__(self, X, y, score_func, n_folds=5, folds_ind=None):
        ClfTestABC.__init__(self, X, y, score_func)
        self.folds_ind = folds_ind 
        self.n_folds = n_folds

    def evaluate(self, clf):
        ytrue, ypred = [], []
        if self.folds_ind is None:
            folds = StratifiedKFold(self.y, 
                                    n_folds=self.n_folds,
                                    shuffle=True,
                                    random_state=None)
        else:
            folds = self.folds_ind

        for train_ind, val_ind in folds:
            clf.fit(self.X[train_ind], self.y[train_ind])
            ypred_ = clf.predict(self.X[val_ind])
            ytrue.append(self.y[val_ind])
            ypred.append(ypred_)
        self.ytrue = np.concatenate(ytrue)
        self.ypred = np.concatenate(ypred)

        return self.score_func(self.ytrue, self.ypred)


class ProfilesLoader:
    def __init__(self, trainset_filename, frq_folder):
        self.trainset_filename = trainset_filename
        self.frq_folder = frq_folder

    def load_data(self):
        df = pd.read_csv(self.trainset_filename, sep = ';')
        n_samples = df.shape[0]
        n_fails = 0
        self.signals = []
        self.y = []

        for idx in range(n_samples):
            if idx % 1000 == 0:
                print str(idx) + "\\" + str(n_samples) + " samples read" 
            try:
                filename = os.path.join(self.frq_folder, df.iloc[idx][0])
                label = df.iloc[idx]['class_name']
                frq = FRQFeatures(filename + '.frq')
                pb = int(frq.info["Pb"])
                pe = int(frq.info["Pe"])
                self.signals.append(frq.signals[0][pb:pe])
                self.y.append(label)

            except Exception as e:
                n_fails += 1
                print e

        print str(n_samples - n_fails) + " samples read"
        print str(n_fails) + " samples failed to read"


    def getXy(self, profile_length):
        X = np.empty((len(self.y), profile_length), dtype=np.float64)
        for idx, signal in enumerate(self.signals):
            p = self.interpolate(signal, profile_length)
            # print len(p)
            #normalize profile
            p = (p - p.min()) / (p.max() - p.min())
            X[idx, :] = p

        return X, self.y


    def interpolate(self, signal, profile_length):
        xp = [i for i in range(len(signal))]
        xnew = np.linspace(0, len(signal), profile_length)
        return np.interp(xnew, xp, signal)



if __name__ == '__main__':
    pass