#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys
import numpy as np
#from sklearn import manifold #was needed for manifold MDS http://scikit-learn.org/stable/auto_examples/manifold/plot_compare_methods.html

#to make it work in console, http://stackoverflow.com/questions/2801882/generating-a-png-with-matplotlib-when-display-is-undefined
#import matplotlib
#matplotlib.use('Agg')

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
import argparse


#http://www.nervouscomputer.com/hfs/cmdscale-in-python/
def cmdscale(D):
    """                                                                                       
    Classical multidimensional scaling (MDS)                                                  
                                                                                               
    Parameters                                                                                
    ----------                                                                                
    D : (n, n) array                                                                          
        Symmetric distance matrix.                                                            
                                                                                               
    Returns                                                                                   
    -------                                                                                   
    Y : (n, p) array                                                                          
        Configuration matrix. Each column represents a dimension. Only the                    
        p dimensions corresponding to positive eigenvalues of B are returned.                 
        Note that each dimension is only determined up to an overall sign,                    
        corresponding to a reflection.                                                        
                                                                                               
    e : (n,) array                                                                            
        Eigenvalues of B.                                                                     
                                                                                               
    """
    # Number of points                                                                        
    n = len(D)
 
    # Centering matrix                                                                        
    H = np.eye(n) - np.ones((n, n))/n
 
    # YY^T                                                                                    
    B = -H.dot(D**2).dot(H)/2
 
    # Diagonalize                                                                             
    evals, evecs = np.linalg.eigh(B)
 
    # Sort by eigenvalue in descending order                                                  
    idx   = np.argsort(evals)[::-1]
    evals = evals[idx]
    evecs = evecs[:,idx]
 
    # Compute the coordinates using positive-eigenvalued components only                      
    w, = np.where(evals > 0)
    L  = np.diag(np.sqrt(evals[w]))
    V  = evecs[:,w]
    Y  = V.dot(L)
 
    return Y, evals

def rand_jitter(arr, jitter):
	stdev = (arr.max()-arr.min()) / 100. * jitter #dispersion proportional to range
	return arr + np.random.randn(len(arr)) * stdev


def read_file(fname, separator):
	distances = np.genfromtxt(fname, delimiter=separator)
	if (distances.shape[0]!=distances.shape[1]):
		print("Matrix is not square:",distances.shape)
	if (distances.shape[0]>distances.shape[1]):
		raise ValueError('More rows than columns?')
	if (distances.shape[0]<distances.shape[1]):
		minsize = min(distances.shape[0],distances.shape[1])
		firstsquarecolumn=distances.shape[1]-minsize
		distances = np.array([row[firstsquarecolumn:] for row in distances]) #this can only fix matrices with more columns than rows
		print("Made the matrix square:",distances.shape)

		#if the file has more columns than rows, assume the first extra column on the left of the square matrix has labels
		labels = np.genfromtxt(fname, delimiter=separator, usecols=firstsquarecolumn-1,dtype=[('label','S10')])
		labels = [label[0].decode("utf-8") for label in labels] 
	else:
		labels = None #no labels
	
	return distances,labels


def compute_mds(distance_matrix, dim):
	embed, evals = cmdscale(distance_matrix)

	variances = [np.var(embed[:,i]) for i in range(len(embed[0]))]
	variances_fraction = [sum(variances[:i+1])/sum(variances) for i in range(len(variances))]
	for i,pv in enumerate(variances_fraction):
		print("In",i+1,"dimensions:",pv)

	dim = min(dim, len(embed[0]))
	embed = np.asarray([embed[:,i] for i in range(dim)]).T

	return embed, variances_fraction[dim-1]


def plot(coordinates, labels, dimensions, variance_fraction, jitter, flipX, flipY, swapXY, outname=""):
	fig = plt.figure()

	if dimensions < 3:
		ax = fig.add_subplot(111)
	else:
		ax = fig.add_subplot(111, projection='3d')

	x_dim = len(coordinates[0])
	y_dim = len(coordinates)

	if flipX:
		coordinates=np.hstack((-coordinates[:, [0]], coordinates[:, [1]]))
	if flipY:
		coordinates=np.hstack((coordinates[:, [0]], -coordinates[:, [1]]))
	if swapXY:
		coordinates[:,[0, 1]] = coordinates[:,[1, 0]]

	add_jitter = lambda tab: rand_jitter(tab, jitter) if jitter>0 else tab
	points = [add_jitter(coordinates[:, i]) for i in range(x_dim)]
	
	if labels is not None and dimensions==2: #could be ported to 3D too
		ax.scatter(*points, alpha=0) #invisible points, because we will show labels instead
		labelconvert={'velland_':'V','velwat_':'W','vpp_':'P','vpa_':'A'} #use this if you want to replace long names with short IDs
		colors={'velland_':'green','velwat_':'blue','vpp_':'red','vpa_':'violet'}
		#for point in points:
		#	print(point)
		for label, x, y in zip(labels, points[0], points[1]):
			color='black'
			for key in labelconvert:
				if label.startswith(key):
					label=labelconvert[key]
					color=colors[key]
			plt.annotate(
				label, 
				xy = (x, y), xytext = (0, 0),
				textcoords = 'offset points', ha = 'center', va = 'center',
				color = color,
				alpha = 0.8,
				#bbox = dict(boxstyle = 'round,pad=0.5', fc = 'yellow', alpha = 0.5),
				#arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3,rad=0')
				)
	else:
		ax.scatter(*points, alpha=0.5)


	plt.title('Projection of phenotype distances, variance preserved = %.1f%%' % (variance_fraction*100))
	plt.tight_layout()
	plt.axis('tight')

	if outname == "":
		plt.show()
	else:
		plt.savefig(outname+".pdf")
		np.savetxt(outname+".csv", coordinates, delimiter=";")


def main(filename, dimensions=3, outname="", jitter=0, separator='\t', flipX=False, flipY=False, swapXY=False):
	distances,labels = read_file(filename, separator)
	embed,variance_fraction = compute_mds(distances, dimensions)

	if dimensions == 1:
		embed = np.array([np.insert(e, 0, 0, axis=0) for e in embed])
	
	plot(embed, labels, dimensions, variance_fraction, jitter, flipX, flipY, swapXY, outname)


if __name__ == '__main__':
	parser = argparse.ArgumentParser()
	parser.add_argument('--in', dest='input', required=True, help='input file with dissimilarity matrix')
	parser.add_argument('--out', dest='output', required=False, help='output file name (without extension)')
	parser.add_argument('--dim', required=False, help='number of dimensions of the new space')
	parser.add_argument('--sep', required=False, help='separator of the source file')
	parser.add_argument('--j', required=False, help='for j>0, random jitter is added to points in the plot')
	parser.add_argument('--flipX', required=False, dest='flipX', action='store_true')
	parser.add_argument('--flipY', required=False, dest='flipY', action='store_true')
	parser.add_argument('--swapXY', required=False, dest='swapXY', action='store_true')
	parser.set_defaults(flipX=False)
	parser.set_defaults(flipY=False)
	parser.set_defaults(swapXY=False)

	args = parser.parse_args()
	set_value = lambda value, default: default if value == None else value
	main(args.input, int(set_value(args.dim, 3)), set_value(args.output, ""), float(set_value(args.j, 0)), set_value(args.sep, "\t"), args.flipX, args.flipY, args.swapXY)
