import random, os,sys, math
import settings
import numpy as np
import scipy
import draw_utils
from sklearn.decomposition import PCA

def pca_decomposition(vector_list, object_name_list, pca_file = None, num_pca_components = 2):

    #make sure all vectors have the same length
    max_size = len(vector_list[0])
    for i in range(1,len(vector_list)):
        if len(vector_list[i]) > max_size:
            max_size = len(vector_list[i])
    for i in range(0,len(vector_list)):
        vector_list[i] += [0] * (max_size - len(vector_list[i]))
    print "PCA with %s" % num_pca_components
    pca = PCA(n_components=num_pca_components)
    pca.fit(vector_list)
    print "BB"
    pca_list = pca.transform(vector_list)

    print 'Explained variance: %s (sum = %s)' % (pca.explained_variance_ratio_,np.sum(pca.explained_variance_ratio_))

    #write down PCA values for each network
    if pca_file is not None:
        if not os.path.exists(os.path.dirname(pca_file)):
            os.makedirs(os.path.dirname(pca_file))

        with open(pca_file,'wb') as f:
            i=0
            for i in range(0,len(object_name_list)):
                f.write('%s\t%s\n' % (object_name_list[i],'\t'.join([str(v) for v in pca_list[i,:]])))
            i+=1

    return pca_list


def load_pca_file(pca_file):
    lines = open(pca_file,'r').readlines()
    pca_list = np.zeros((len(lines),len(lines[0].split('\t'))-1))
    for i in range(0,len(lines)):
        pca_list[i,:] = lines[i].split('\t')[1:]
    return pca_list


def draw_pca(pca_list, object_list, plots_dir, file_name, figure_format_list = 'eps', object_to_class_func = None):
    scatter_points = []
    for i in range(0,len(object_list)):
        object_class = object_to_class_func(object_list[i]) if object_to_class_func is not None else object_list[i]
        scatter_points.append(np.concatenate((pca_list[i,:],[object_class])))

    if not isinstance(figure_format_list,list):
        figure_format_list = [figure_format_list]

    draw_params = {
        'title': '',
        'prop_x_title': 'PCA-1',
        'prop_y_title': 'PCA-2',
        'prop_z_title': 'PCA-3',
        'different_markers': True,
        'hide_ticks': True,
        '3d': len(pca_list[0,:]) == 3
    }

    for figure_format in figure_format_list:
        draw_utils.draw_scatter_plot(scatter_points,plots_dir+'%s/' % figure_format, file_name, format = figure_format, params = draw_params)


def calculate_pairwise_distances(pca_vector_list, object_names, distance_measure = 'pca', sort_distances = False, output_file = None, ignore_pairs_ground_truth_value = None, summary_distances_file = None, object_to_class_func = None):
    """
    Calculate pairwise distances between list of vectors
    :param ignore_pairs_ground_truth:
    :param summary_distances_file: Save top k distances with class names for reference
    :param pca_vector_list:
    :param object_names:
    :param distance_measure:
    :param sort_distances:
    :param output_file:
    :param ignore_ground_truth_pairs: If two objects has this class, do not compare them (this makes sense for bio data - not interested in "negative" pairs)
    :param object_to_class_func:
    :return:
    """
    num_vectors = len(pca_vector_list)
    num_pairs = (num_vectors * (num_vectors-1))/2
    object_distances = np.zeros(num_pairs, dtype=('f8,a30,a30'))
    k=0

    num_of_ignored_pairs = 0
    if ignore_pairs_ground_truth_value is not None:
        print 'Ignoring all pairs where both objects have ground truth class \'%s\'' % ignore_pairs_ground_truth_value
        for i in xrange(0,num_vectors):
            class_1 = object_to_class_func(object_names[i]) if object_to_class_func is not None else object_names[i]
            if class_1 == ignore_pairs_ground_truth_value:
                num_of_ignored_pairs += 1
        num_of_ignored_pairs = (num_of_ignored_pairs * (num_of_ignored_pairs - 1)) /2
        print '%s pairs will be ignored from %s elements' % (num_of_ignored_pairs,num_vectors)

    for i in xrange(0,num_vectors-1):
        object_name_1 = object_names[i]
        class_1 = object_to_class_func(object_name_1) if object_to_class_func is not None else object_name_1
        vector_1 = pca_vector_list[i]#,:]

        for j in xrange(i+1,num_vectors):
            object_name_2 = object_names[j]
            class_2 = object_to_class_func(object_name_2) if object_to_class_func is not None else object_name_2
            if ignore_pairs_ground_truth_value is not None and class_1 == class_2 == ignore_pairs_ground_truth_value:
                continue

            vector_2 = pca_vector_list[j]#,:]

            if distance_measure == 'spearman':
                corr = scipy.stats.spearmanr(vector_1,vector_2)[0]
                dist = 1-corr
            elif distance_measure == 'pearson':
                corr = np.corrcoef(vector_1,vector_2)[0,1]
                dist = 1-corr
            elif 'pca' in distance_measure:
                dist =  scipy.spatial.distance.euclidean(vector_1,vector_2)
            elif distance_measure == 'rgf':
                t1 = float(sum(vector_1))
                t2 = float(sum(vector_2))

                dist = 0
                for l in xrange(0,min(len(vector_1),len(vector_2))):
                   f1=0
                   f2=0
                   if vector_1[l] != 0 and t1 != 0:
                    f1 -= math.log(vector_1[l]/t1)
                   if vector_2[l] != 0 and t2 != 0:
                    f2 -= math.log(vector_2[l]/t2)
                   dist += math.fabs(f1 - f2)
            else:
                raise Exception('Unknown distance measure %s' % distance_measure)

            object_distances[k] = (dist,object_name_1,object_name_2)

            sys.stdout.write('\r%.2f%%...' % (100.0 * k / (num_pairs-num_of_ignored_pairs)))
            sys.stdout.flush()

            k+=1

    if ignore_pairs_ground_truth_value is not None:
        object_distances = np.resize(object_distances,(k))

    print 'Calculated %s pairwise distances %s' % (
        len(object_distances),
        ('(out of %s possible pairs (%.2f%%) - excluded values with \'%s\' ground truth value)' %
         (num_pairs, 100.0 * len(object_distances) / num_pairs,ignore_pairs_ground_truth_value)) if ignore_pairs_ground_truth_value is not None else ''
    )

    if sort_distances:
        print 'Sorting distances...'
        k_list = object_distances.argsort(axis = 0) #sorted indices - may need to correlate later two pair orderings
        object_distances = object_distances[k_list]
    else:
        k_list = range(0,len(object_distances))

    if output_file is not None:
        print 'Saving distances to file...'

        if not os.path.exists(os.path.dirname(output_file)):
            os.makedirs(os.path.dirname(output_file))

        with open(output_file,'wb') as f:
            for i in xrange(0,len(object_distances)):
                value = object_distances[i]
                f.write('%s\t%s\t%s\n' % (value[0],value[1],value[2]))

    if summary_distances_file is not None:
        print 'Saving class distances (first 1000 records)...'

        if not os.path.exists(os.path.dirname(summary_distances_file)):
            os.makedirs(os.path.dirname(summary_distances_file))

        with open(summary_distances_file,'wb') as f:
            for i in xrange(0,len(object_distances[:1000])):
                value = object_distances[i]
                class_1 = object_to_class_func(value[1]) if object_to_class_func is not None else value[1]
                class_2 = object_to_class_func(value[2]) if object_to_class_func is not None else value[2]
                f.write('%s\t%s\t%s\t%s\t%s\t%s\n' % (value[0],class_1,class_2,value[1],value[2],k_list[i]))

    return object_distances

def load_pairwise_distances(input_file):
    object_distances = []
    with open(input_file,'r') as f:
        for line in f.readlines():
            values = line.strip().split('\t')
            values[0] = float(values[0])
            object_distances.append(values)

    return object_distances

def get_top_k_pairs_from_distance_file(input_file,k):
    pairs = []
    with open(input_file,'r') as f:
        num_lines = sum(1 for line in f)
        n_max = k * num_lines if k<1.1 else k
        i=0
        f.seek(0)
        for line in f:
            if i >= n_max:
                break
            values = line.replace('\n','').split('\t')
            pairs.append((values[1],values[2]))
            i+=1
    return pairs


#for random method comparison
def generate_random_points(n, dimension=2):
    points = []
    for i in range(0,n):
        p = []
        for d in range(0,dimension):
            p.append(random.uniform(0,1))
        points.append(p)
    return points
