import numpy as np
import sklearn
from sklearn.metrics import roc_curve, auc
import sklearn.metrics
import sklearn.model_selection
import copy
from . import util
import time
from . import metrics as ranking_metrics
from optimal_guide_finder.Azimuth_Model.models import regression
from optimal_guide_finder.Azimuth_Model.models import ensembles
from optimal_guide_finder.Azimuth_Model.models import DNN
from optimal_guide_finder.Azimuth_Model.models import baselines
import multiprocessing


def fill_in_truth_and_predictions(truth, predictions, fold, y_all, y_pred, learn_options, test):
    truth[fold]['ranks'] = np.hstack((truth[fold]['ranks'],
                                      y_all[learn_options['rank-transformed target name']].values[test].flatten()))

    truth[fold]['thrs'] = np.hstack((truth[fold]['thrs'],
                                     y_all[learn_options['binary target name']].values[test].flatten()))

    if 'raw_target_name' in list(learn_options.keys()):
        truth[fold]['raw'] = np.hstack((truth[fold]['raw'],
                                        y_all[learn_options['raw target name']].values[test].flatten()))

    predictions[fold] = np.hstack((predictions[fold], y_pred.flatten()))

    return truth, predictions


def construct_filename(learn_options, TEST):
    if "V" in learn_options:
        filename = "V%s" % learn_options["V"]
    else:
        filename = "offV1"

    if TEST:
        filename = "TEST."

    filename += learn_options["method"]
    filename += '.order%d' % learn_options["order"]
    # try:
    #     learn_options["target_name"] = ".%s" % learn_options["target_name"].split(" ")[1]
    # except:
    #     pass
    filename += learn_options["target_name"]
    if learn_options["method"] == "GPy":
        pass
        # filename += ".R%d" % opt_options['num_restarts']
        # filename += ".K%s" % learn_options['kerntype']
        # if learn_options.has_key('degree'):
        #     filename += "d%d" % learn_options['degree']
        # if learn_options['warped']:
        #     filename += ".Warp"
    elif learn_options["method"] == "linreg":
        filename += "." + learn_options["penalty"]
    filename += "." + learn_options["cv"]

    if learn_options["training_metric"] == "NDCG":
        filename += ".NDGC_%d" % learn_options["NDGC_k"]
    elif learn_options["training_metric"] == "AUC":
        filename += ".AUC"
    elif learn_options["training_metric"] == 'spearmanr':
        filename += ".spearman"

    print("filename = %s" % filename)
    return filename

def print_summary(global_metric, results, learn_options, feature_sets, flags):
    print("\nSummary:")
    print(learn_options)
    print("\t\tglobal %s=%.2f" % (learn_options['metric'], global_metric))
    print("\t\tmedian %s across folds=%.2f" % (learn_options['metric'], np.median(results[0])))
    print("\t\torder=%d" % learn_options["order"])
    if 'kerntype' in learn_options: "\t\tkern type = %s" % learn_options['kerntype']
    if 'degree' in learn_options: print("\t\tdegree=%d" % learn_options['degree'])
    print("\t\ttarget_name=%s" % learn_options["target_name"])

    for k in list(flags.keys()):
        print('\t\t' + k + '=' + str(learn_options[k]))

    print("\t\tfeature set:")
    for set in list(feature_sets.keys()):
        print("\t\t\t%s" % set)
    print("\t\ttotal # features=%d" % results[4])

def extract_fpr_tpr_for_fold(aucs, fold, i, predictions, truth, y_binary, test, y_pred):
    assert len(np.unique(y_binary))<=2, "if using AUC need binary targets"
    fpr, tpr, _ = roc_curve(y_binary[test], y_pred)
    roc_auc = auc(fpr, tpr)
    aucs.append(roc_auc)

def extract_NDCG_for_fold(metrics, fold, i, predictions, truth, y_ground_truth, test, y_pred, learn_options):
    NDCG_fold = ranking_metrics.ndcg_at_k_ties(y_ground_truth[test].flatten(), y_pred.flatten(), learn_options["NDGC_k"])
    metrics.append(NDCG_fold)

def extract_spearman_for_fold(metrics, fold, i, predictions, truth, y_ground_truth, test, y_pred, learn_options):
    spearman = util.spearmanr_nonan(y_ground_truth[test].flatten(), y_pred.flatten())[0]
    assert not np.isnan(spearman), "found nan spearman"
    metrics.append(spearman)

def get_train_test(test_gene, y_all, train_genes=None):
    # this is a bit convoluted because the train_genes+test_genes may not add up to all genes
    # for e.g. when we load up V3, but then use only V2, etc.

    not_test = (y_all.index.get_level_values('Target gene').values != test_gene)

    if train_genes is not None:
        in_train_genes = np.zeros(not_test.shape, dtype=bool)
        for t_gene in train_genes:
            in_train_genes = np.logical_or(in_train_genes, (y_all.index.get_level_values('Target gene').values == t_gene))
        train = np.logical_and(not_test, in_train_genes)
    else:
        train = not_test
    #y_all['test'] as to do with extra pairs in V2
    if test_gene == 'dummy':
        test = train
    else:
         test = (y_all.index.get_level_values('Target gene').values== test_gene)

    # convert to indices
    test = np.where(test == True)[0]
    train = np.where(train == True)[0]
    return train, test


def cross_validate(y_all, feature_sets, learn_options=None, TEST=False, train_genes=None, CV=True):
    '''
    feature_sets is a dictionary of "set name" to pandas.DataFrame
    one set might be single-nucleotide, position-independent features of order X, for e.g.
    Method: "GPy" or "linreg"
    Metric: NDCG (learning to rank metric, Normalized Discounted Cumulative Gain); AUC
    Output: cv_score_median, gene_rocs
    When CV=False, it trains on everything (and tests on everything, just to fit the code)
    '''

    print("range of y_all is [%f, %f]" % (np.min(y_all[learn_options['target_name']].values), np.max(y_all[learn_options['target_name']].values)))

    allowed_methods = ["GPy", "linreg", "AdaBoostRegressor", "AdaBoostClassifier",
                       "DecisionTreeRegressor", "RandomForestRegressor",
                       "ARDRegression", "GPy_fs", "mean", "random", "DNN",
                       "lasso_ensemble", "doench", "logregL1", "sgrna_from_doench", 'SVC', 'xu_et_al']

    assert learn_options["method"] in allowed_methods,"invalid method: %s" % learn_options["method"]
    assert learn_options["method"] == "linreg" and learn_options['penalty'] == 'L2' or learn_options["weighted"] is None, "weighted only works with linreg L2 right now"

    # construct filename from options
    filename = construct_filename(learn_options, TEST)

    print("Cross-validating genes...")
    t2 = time.time()

    y = np.array(y_all[learn_options["target_name"]].values[:,None],dtype=np.float64)

    # concatenate feature sets in to one nparray, and get dimension of each
    inputs, dim, dimsum, feature_names = util.concatenate_feature_sets(feature_sets)
    #import pickle; pickle.dump([y, inputs, feature_names, learn_options], open("saved_models/inputs.p", "wb" )); import ipdb; ipdb.set_trace()

    if not CV:
        assert learn_options['cv'] == 'gene', 'Must use gene-CV when CV is False (I need to use all of the genes and stratified complicates that)'

    # set-up for cross-validation
    ## for outer loop, the one Doench et al use genes for
    if learn_options["cv"] == "stratified":
        assert "extra_pairs" not in learn_options or learn_options['extra pairs'], "can't use extra pairs with stratified CV, need to figure out how to properly account for genes affected by two drugs"
        label_encoder = sklearn.preprocessing.LabelEncoder()
        label_encoder.fit(y_all['Target gene'].values)
        gene_classes = label_encoder.transform(y_all['Target gene'].values)
        if 'n_folds' in list(learn_options.keys()):
            n_folds = learn_options['n_folds']
        elif learn_options['train_genes'] is not None and learn_options["test_genes"] is not None:
            n_folds = len(learn_options["test_genes"])
        else:
            n_folds = len(learn_options['all_genes'])

        cv = sklearn.cross_validation.StratifiedKFold(gene_classes, n_folds=n_folds, shuffle=True)
        fold_labels = ["fold%d" % i for i in range(1,n_folds+1)]
        if learn_options['num_genes_remove_train'] is not None:
            raise NotImplementedException()
    elif learn_options["cv"]=="gene":
        cv = []

        if not CV:
            train_test_tmp = get_train_test('dummy', y_all) # get train, test split using a dummy gene
            #train_tmp, test_tmp = train_test_tmp
            # not a typo, using training set to test on as well, just for this case. Test set is not used
            # for internal cross-val, etc. anyway.
            #train_test_tmp = (train_tmp, train_tmp)
            cv.append(train_test_tmp)
            fold_labels = ["dummy_for_no_cv"]#learn_options['all_genes']

        elif learn_options['train_genes'] is not None and learn_options["test_genes"] is not None:
            assert learn_options['train_genes'] is not None and learn_options['test_genes'] is not None, "use both or neither"
            for i, gene in enumerate(learn_options['test_genes']):
                cv.append(get_train_test(gene, y_all, learn_options['train_genes']))
            fold_labels = learn_options["test_genes"]
            # if train and test genes are seperate, there should be only one fold
            train_test_disjoint = set.isdisjoint(set(learn_options["train_genes"].tolist()), set(learn_options["test_genes"].tolist()))

        else:
            for i, gene in enumerate(learn_options['all_genes']):
                train_test_tmp = get_train_test(gene, y_all)
                cv.append(train_test_tmp)
            fold_labels = learn_options['all_genes']

        if learn_options['num_genes_remove_train'] is not None:
            for i, (train,test) in enumerate(cv):
                unique_genes = np.random.permutation(np.unique(np.unique(y_all['Target gene'][train])))
                genes_to_keep = unique_genes[0:len(unique_genes) - learn_options['num_genes_remove_train']]
                guides_to_keep = []
                filtered_train = []
                for j, gene in enumerate(y_all['Target gene']):
                    if j in train and gene in genes_to_keep:
                        filtered_train.append(j)
                cv_i_orig = copy.deepcopy(cv[i])
                cv[i] = (filtered_train, test)
                if learn_options['num_genes_remove_train']==0:
                    assert np.all(cv_i_orig[0]==cv[i][0])
                    assert np.all(cv_i_orig[1]==cv[i][1])
                print("# train/train after/before is %s, %s" % (len(cv[i][0]), len(cv_i_orig[0])))
                print("# test/test after/before is %s, %s" % (len(cv[i][1]), len(cv_i_orig[1])))
    else:
        raise Exception("invalid cv options given: %s" % learn_options["cv"])

    cv = [c for c in cv] #make list from generator, so can subset for TEST case
    if TEST:
        ind_to_use = [0]#[0,1]
        cv = [cv[i] for i in ind_to_use]
        fold_labels = [fold_labels[i] for i in ind_to_use]

    truth =  dict([(t, dict([(m, np.array([])) for m in ['raw', 'ranks', 'thrs']])) for t in fold_labels])
    predictions =  dict([(t, np.array([])) for t in fold_labels])

    m = {}
    metrics = []

    #do the cross-validation
    num_proc = learn_options["num_proc"]
    if num_proc > 1:
        num_proc = np.min([num_proc,len(cv)])
        print("using multiprocessing with %d procs--one for each fold" % num_proc)
        jobs = []
        pool = multiprocessing.Pool(processes=num_proc)
        for i,fold in enumerate(cv):
            train,test = fold
            print("working on fold %d of %d, with %d train and %d test" % (i, len(cv), len(train), len(test)))
            if learn_options["method"]=="GPy":
                job = pool.apply_async(Azimuth_Model.models.GP.gp_on_fold, args=(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options))
            elif learn_options["method"]=="linreg":
                job = pool.apply_async(Azimuth_Model.models.regression.linreg_on_fold, args=(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options))
            elif learn_options["method"]=="logregL1":
                job = pool.apply_async(Azimuth_Model.models.regression.logreg_on_fold, args=(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options))
            elif learn_options["method"]=="AdaBoostRegressor":
                 job = pool.apply_async(Azimuth_Model.models.ensembles.adaboost_on_fold, args=(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options, False))
            elif learn_options["method"]=="AdaBoostClassifier":
                 job = pool.apply_async(Azimuth_Model.models.ensembles.adaboost_on_fold, args=(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options, True))
            elif learn_options["method"]=="DecisionTreeRegressor":
                job = pool.apply_async(Azimuth_Model.models.ensembles.decisiontree_on_fold, args=(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options))
            elif learn_options["method"]=="RandomForestRegressor":
                job = pool.apply_async(Azimuth_Model.models.ensembles.randomforest_on_fold, args=(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options))
            elif learn_options["method"]=="ARDRegression":
                job = pool.apply_async(Azimuth_Model.models.regression.ARDRegression_on_fold, args=(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options))
            elif learn_options["method"] == "random":
                job = pool.apply_async(Azimuth_Model.models.baselines.random_on_fold, args=(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options))
            elif learn_options["method"] == "mean":
                job = pool.apply_async(Azimuth_Model.models.baselines.mean_on_fold, args=(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options))
            elif learn_options["method"] == "SVC":
                job = pool.apply_async(Azimuth_Model.models.baselines.SVC_on_fold, args=(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options))
            elif learn_options["method"] == "DNN":
                job = pool.apply_async(Azimuth_Model.models.DNN.DNN_on_fold, args=(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options))
            elif learn_options["method"] == "lasso_ensemble":
                job = pool.apply_async(Azimuth_Model.models.ensembles.LASSOs_ensemble_on_fold, args=(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options))
            elif learn_options["method"] == "doench":
                    job = pool.apply_async(Azimuth_Model.models.baselines.doench_on_fold, args=(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options))
            elif learn_options["method"] == "sgrna_from_doench":
                    job = pool.apply_async(Azimuth_Model.models.baselines.sgrna_from_doench_on_fold, args=(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options))
            elif learn_options["method"] == "xu_et_al":
                    job = pool.apply_async(Azimuth_Model.models.baselines.xu_et_al_on_fold, args=(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options))
            else:
                raise Exception("did not find method=%s" % learn_options["method"])
            jobs.append(job)
        pool.close()
        pool.join()
        for i,fold in enumerate(cv):#i in range(0,len(jobs)):
            y_pred, m[i] = jobs[i].get()
            train,test = fold

            if learn_options["training_metric"]=="AUC":
                extract_fpr_tpr_for_fold(metrics, fold_labels[i], i, predictions, truth, y_all[learn_options["ground_truth_label"]].values, test, y_pred)
            elif learn_options["training_metric"]=="NDCG":
                extract_NDCG_for_fold(metrics, fold_labels[i], i, predictions, truth, y_all[learn_options["ground_truth_label"]].values, test, y_pred, learn_options)
            elif learn_options["training_metric"] == 'spearmanr':
                 extract_spearman_for_fold(metrics, fold_labels[i], i, predictions, truth, y_all[learn_options["ground_truth_label"]].values, test, y_pred, learn_options)
            else:
                raise Exception("invalid 'training_metric' in learn_options: %s" % learn_options["training_metric"])

            truth, predictions = fill_in_truth_and_predictions(truth, predictions, fold_labels[i], y_all, y_pred, learn_options, test)

        pool.terminate()

    else:
        # non parallel version
        for i,fold in enumerate(cv):
            train,test = fold
            if learn_options["method"]=="GPy":
                y_pred, m[i] = gp_on_fold(Azimuth_Model.models.GP.feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options)
            elif learn_options["method"]=="linreg":
                y_pred, m[i] = Azimuth_Model.models.regression.linreg_on_fold(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options)
            elif learn_options["method"]=="logregL1":
                y_pred, m[i] = Azimuth_Model.models.regression.logreg_on_fold(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options)
            elif learn_options["method"]=="AdaBoostRegressor":
                 y_pred, m[i] = Azimuth_Model.models.ensembles.adaboost_on_fold(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options, classification=False)
            elif learn_options["method"]=="AdaBoostClassifier":
                 y_pred, m[i] = Azimuth_Model.models.ensembles.adaboost_on_fold(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options, classification=True)
            elif learn_options["method"]=="DecisionTreeRegressor":
                 y_pred, m[i] = Azimuth_Model.models.ensembles.decisiontree_on_fold(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options)
            elif learn_options["method"]=="RandomForestRegressor":
                 y_pred, m[i] = Azimuth_Model.models.ensembles.randomforest_on_fold(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options)
            elif learn_options["method"]=="ARDRegression":
                 y_pred, m[i] = Azimuth_Model.models.regression.ARDRegression_on_fold(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options)
            elif learn_options["method"]=="GPy_fs":
                 y_pred, m[i] = Azimuth_Model.models.GP.gp_with_fs_on_fold(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options)
            elif learn_options["method"] == "random":
                 y_pred, m[i] = Azimuth_Model.models.baselines.random_on_fold(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options)
            elif learn_options["method"] == "mean":
                 y_pred, m[i] = Azimuth_Model.models.baselines.mean_on_fold(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options)
            elif learn_options["method"] == "SVC":
                 y_pred, m[i] = Azimuth_Model.models.baselines.SVC_on_fold(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options)
            elif learn_options["method"] == "DNN":
                 y_pred, m[i] = Azimuth_Model.models.DNN.DNN_on_fold(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options)
            elif learn_options["method"] == "lasso_ensemble":
                 y_pred, m[i] = Azimuth_Model.models.ensembles.LASSOs_ensemble_on_fold(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options)
            elif learn_options["method"] == "doench":
                 y_pred, m[i] = Azimuth_Model.models.baselines.doench_on_fold(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options)
            elif learn_options["method"] == "sgrna_from_doench":
                 y_pred, m[i] = Azimuth_Model.models.baselines.sgrna_from_doench_on_fold(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options)
            elif learn_options["method"] == "xu_et_al":
                 y_pred, m[i] = Azimuth_Model.models.baselines.xu_et_al_on_fold(feature_sets, train, test, y, y_all, inputs, dim, dimsum, learn_options)
            else:
                raise Exception("invalid method found: %s" % learn_options["method"])

            if learn_options["training_metric"]=="AUC":
                # fills in truth and predictions
                extract_fpr_tpr_for_fold(metrics, fold_labels[i], i, predictions, truth, y_all[learn_options['ground_truth_label']].values, test, y_pred)
            elif learn_options["training_metric"]=="NDCG":
                extract_NDCG_for_fold(metrics, fold_labels[i], i, predictions, truth, y_all[learn_options["ground_truth_label"]].values, test, y_pred, learn_options)
            elif learn_options["training_metric"] == 'spearmanr':
                extract_spearman_for_fold(metrics, fold_labels[i], i, predictions, truth, y_all[learn_options["ground_truth_label"]].values, test, y_pred, learn_options)

            truth, predictions = fill_in_truth_and_predictions(truth, predictions, fold_labels[i], y_all, y_pred, learn_options, test)

            print("\t\tRMSE: ", np.sqrt(((y_pred - y[test])**2).mean()))
            print("\t\tSpearman correlation: ", util.spearmanr_nonan(y[test], y_pred)[0])
            print("\t\tfinished fold/gene %i of %i" % (i+1, len(fold_labels)))


    cv_median_metric =[np.median(metrics)]
    gene_pred = [(truth, predictions)]
    print("\t\tmedian %s across gene folds: %.3f" % (learn_options["training_metric"], cv_median_metric[-1]))

    t3 = time.time()
    print("\t\tElapsed time for cv is %.2f seconds" % (t3-t2))
    return metrics, gene_pred, fold_labels, m, dimsum, filename, feature_names
