"""
Author(s): Steven van den Broek, Yuqin Cui, Sam Baggen, Tom Udding
Created: 2019-05-05
Edited: 2019-06-19
"""
from colorcet import palette
from fastcluster import linkage
from holoviews import HeatMap
from holoviews.plotting.bokeh.callbacks import LinkCallback
from holoviews.plotting.links import Link
from numpy import arange
from pandas import DataFrame, read_hdf
from panel import extension
from param import depends, ObjectSelector, Parameterized
from scipy.spatial.distance import pdist, squareform

def makeMatrix(file, sid, df=False):
    if not df:
        df = read_hdf(file)
    else:
        df = file

    names = df.columns.tolist()
    df_original = df.copy()

    def remove_noise(df):
        noise = []
        global names
        # print(names)
        names = df.columns.tolist()

        for name in names:
            if (len(df[name][df[name] == 0]) == len(names)):
                noise.append(name)

        for name in noise:
            names.remove(name)

        df.drop(noise, inplace=True)
        df.drop(noise, axis=1, inplace=True)
        names = df.columns.tolist()
        return df

    def invert_df(df):
        for name in df.columns:
            df[name] = 1 - df[name]

        return df
        # print(df)
    # %%
    # This is just the method online: https://gmarti.gitlab.io/ml/2017/09/07/how-to-sort-distance-matrix.html
    # We have to clean data and modified the method

    # %%
    # The output of linkage is stepwise dendrogram,
    # which is represented as an (N − 1) × 4 NumPy array with floating point entries (dtype=numpy.double).
    # The first two columns contain the node indices which are joined in each step. The input nodes are
    # labeled 0,..., N − 1, and the newly generated nodes have the labels N,...2N-2.
    # The third column contains the distance between the two nodes at each step, ie. the
    # current minimal distance at the time of the merge. The fourth column counts the
    # number of points which comprise each new node.

    # Idea is from: https://gmarti.gitlab.io/ml/2017/09/07/how-to-sort-distance-matrix.html

    # Traversal the hierarhical tree generated by linkage
    def traversal_tree(hier_tree, number_of_node, current_index):
        if current_index < number_of_node:
            return [current_index]
        else:
            return (traversal_tree(hier_tree, number_of_node, int(hier_tree[current_index - number_of_node][1])) +
                    traversal_tree(hier_tree, number_of_node, int(hier_tree[current_index - number_of_node][0])))

    # %%
    def compute_serial_matrix(df, method="ward", dist_metric="euclidean", calc_pdist=True):
        # define the dist_mat by different dist_metric mathod in fast_clustering package
        if calc_pdist:
            dist_mat = squareform(pdist(df, metric=dist_metric))
        else:
            dist_mat = df
        # hierar tree was got from package "fast-clustering"
        hierar_tree = linkage(squareform(dist_mat), method=method, preserve_input=True)
        # The order implied by the hierarhical tree
        reordered_index = traversal_tree(hierar_tree, len(dist_mat), 2 * len(dist_mat) - 2)
        return reordered_index, hierar_tree

    # linkage(squareform(pdist(df, metric="euclidean")), method="ward",preserve_input=True)
    # %%

    # %%
    # order original matrix based on index provided
    def author_reorder_list(df, order):
        new_names = df.columns.tolist()
        return [new_names[i] for i in order]

    def reordercol(df, order):
        secondIndex = []
        new_df = df
        new_df['nindex'] = arange(len(new_df))
        for i in order:
            secondIndex += new_df.index[new_df['nindex'] == i].tolist()
        new_df.drop('nindex', axis=1, inplace=True)
        a = new_df.reindex(index=secondIndex)
        return a

    def reorderrow(df, order):
        a = df.values
        permutation = order
        return a[:, permutation]

    def reorder_input_df(df, order):
        reorder_col = reordercol(df, order)
        finish = reorderrow(reorder_col, order)
        return finish

    # %%
    def to_liquid(matrix):
        # print(len(names))
        solid = DataFrame(matrix.copy())
        solid.index = names
        solid.columns = names
        solid.reset_index(inplace=True)
        liquid = solid.melt(id_vars='index', value_vars=list(names), var_name="name2")
        liquid.columns = ['index2', 'index1', 'value']
        liquid = liquid[['index1', 'index2', 'value']]
        # print(liquid)
        return liquid

    # %%
    def to_liquid_2(matrix, df, order):
        solid = DataFrame(matrix)
        name_list = author_reorder_list(df, order)
        # print(name_list)
        solid.index = name_list
        solid.columns = name_list
        solid.reset_index(inplace=True)
        liquid = solid.melt(id_vars='index', value_vars=list(name_list[0:]), var_name="name2")
        liquid.columns = ['index2', 'index1', 'value']
        liquid = liquid[['index1', 'index2', 'value']]
        # print(liquid)
        return liquid

    def dis_to_similarity(grid):
        nrows = len(grid)
        ncols = len(grid[0])
        for i in range(nrows):
            for j in range(ncols):
                grid[i][j] = 1 - grid[i][j]

    #extension()

    class SelectLink(Link):
        _requires_target = True

    class SelectCallback(LinkCallback):

        source_model = 'selected'
        # source_handles = ['cds']
        on_source_changes = ['indices']

        target_model = 'selected'

        source_code = "let len = {}".format(len(names)) + """
            let new_indices = []
            for (let i = 0; i < source_selected.indices.length; i++){
                let index = source_selected.indices[i]
                j = len-1-(index%len)+Math.floor(index/(len))*(len)
                new_indices[i] = j
            }
            target_selected.indices = new_indices
        """

    SelectLink.register_callback('bokeh', SelectCallback)

    # table = Table(current_data)
    # SelectLink(hm, table)
    # SelectLink(table, hm)

    class SelectedDataLink(Link):
        _requires_target = True

    class SelectedDataCallback(LinkCallback):

        source_model = 'selected'
        source_handles = ['cds']
        on_source_changes = ['indices']

        target_model = 'cds'

        source_code = "let len = {}".format(len(names)) + """
            let new_indices = []
            for (let i = 0; i < source_selected.indices.length; i++){
                let index = source_selected.indices[i]
                j = len-1-(index%len)+Math.floor(index/(len))*(len)
                new_indices[i] = j
            }
            var inds = source_selected.indices
            var d = source_cds.data

            selected_data = {}
            selected_data['index1'] = []
            selected_data['index2'] = []
            selected_data['value'] = []
            selected_data['zvalues'] = []

            for (var i = 0; i < inds.length; i++){
                selected_data['index1'].push(d['index1'][inds[i]])
                selected_data['index2'].push(d['index2'][inds[i]])
                selected_data['value'].push(d['value'][inds[i]])
                selected_data['zvalues'].push(d['zvalues'][inds[i]])
            }
            target_cds.data = selected_data

        """

    SelectedDataLink.register_callback('bokeh', SelectedDataCallback)
    from graphion.session.handler import calculate_plot_size

    class Matrix_dropdown(Parameterized):
        reordering = ObjectSelector(default="ward",
                                          objects=["none", "single", "average", "complete", "centroid", "weighted",
                                                   "median", "ward"])
        metric = ObjectSelector(default="euclidean",
                                      objects=["euclidean", "minkowski", "cityblock", "sqeuclidean", "cosine",
                                               "correlation", "hamming", "jaccard", "chebyshev", "canberra",
                                               "braycurtis"])
        color_palette = ObjectSelector(default='cividis',
                                             objects=['kbc', 'kgy', 'bgy', 'bmw', 'bmy', 'cividis', 'dimgray', 'fire',
                                                      'inferno', 'viridis'])

        def __init__(self, calc_pdist=True, show_only_selection=True):
            self.show_only_selection = show_only_selection
            self.calc_pdist = calc_pdist
            from graphion.session.handler import calculate_plot_size
            self.size = calculate_plot_size(sid)
            super(Matrix_dropdown, self).__init__()
            self.height = calculate_plot_size(sid)
            self.width = int(calculate_plot_size(sid)*16/15)


        @depends('reordering', 'metric', 'color_palette')
        def view(self):
            if self.reordering == "none":
                result = to_liquid(df_original.copy().values)

                hm = HeatMap(result).opts(tools=['tap', 'box_select', 'hover'], active_tools=['box_select'],
                                             height=self.height, width=self.width, xaxis=None, yaxis=None, cmap=self.color_palette,
                                             colorbar=True, toolbar='above')

                # FOR LINKING TESTING
                # current_data = hm.data
                # table = Table(current_data)
                # table.opts(height=500)
                #
                # if self.show_only_selection:
                #     SelectedDataLink(hm, table)
                # else:
                #     SelectLink(hm, table)
                #     SelectLink(table, hm)

                # return pn.Row(hm, table)
                return hm

            else:
                inverted = invert_df(df.copy())

                res_order, res_linkage = compute_serial_matrix(inverted, self.reordering, dist_metric=self.metric,
                                                               calc_pdist=self.calc_pdist)

                reordered_matrix_col = reordercol(inverted, res_order)
                reordered_matrix = reorderrow(reordered_matrix_col, res_order)
                dis_to_similarity(reordered_matrix)

                result = to_liquid_2(reordered_matrix, inverted, res_order)

                hm = HeatMap(result).opts(tools=['tap', 'box_select', 'hover'], active_tools=['box_select'],
                                             height=self.height, width=self.width, xaxis=None, yaxis=None, cmap=self.color_palette,
                                             colorbar=True, toolbar='above')
                # FOR LINKING TESTING
                # table = Table(hm.data)
                # table.opts(height=500)
                #
                # if self.show_only_selection:
                #     SelectedDataLink(hm, table)
                # else:
                #     SelectLink(hm, table)
                #     SelectLink(table, hm)
                #
                # return pn.Row(hm, table)
                return hm

    matrix = Matrix_dropdown(sid)

    # %%
    # hv_plot = hm + table
    #matrix_pane1 = pn.Column(pn.Pane(matrix.param, css_classes=['matrix_dropdowns']), matrix.view)
    # matrix_pane2 = pn.Column(matrix.param, matrix.view(show_only_selection=False))

    return matrix
