#!/usr/bin/env python
# coding: utf-8
import warnings
import pandas as pd
import numpy as np
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
from sklearn.cluster import DBSCAN
from sklearn import preprocessing
import matplotlib.pyplot as plt
import seaborn as sns

print('Random State is 42 by default, you can change it in the functions parameters.')
print('Functions still in build : ACP, Hierarchical Clustering, allClustering. Do not use it')

# # Class data preprocessing
class prepData:
    """This class is used to preprocess the data"""
    def centrageReduction(data):
        """This function is used to scale the data"""
        X = data.values
        std_scale = preprocessing.StandardScaler().fit(X)
        x_scaled = std_scale.transform(X)
        return x_scaled
    #def acp():
        #"""This function is not implemented yet, still in build"""
        #pass
# # Class Clustering

class clustering:
    """Class for clustering"""
    def elbowMethod(data, x_scaled, K, random_s=42):
        "Function that compute the distortion for each number of clusters"
        np.random.seed(random_s)
        distortions = []
        index_ = []
        for k in K:
            print("Step: {} running...".format(k))
            index_.append(k)
            kmeanModel = KMeans(n_clusters=k)
            kmeanModel.fit(x_scaled)
            distortions.append(kmeanModel.inertia_)
        plt.figure(figsize=(16, 8))
        plt.plot(K, distortions, 'bx-')
        plt.xlabel('k')
        plt.ylabel('Distortion')
        plt.title('The Elbow Method showing the optimal k')
        plt.show()

    def silhouette(data, x_scaled, K, random_s=42):
        """Function that computes the silhouette coef for a given numbers of clusters. \n
        Print a scatter plot of each coef in function of the coef.
        Return a sorted df with the silhouette coef"""
        #Random seed fixed
        np.random.seed(random_s)
        #Init silouhette list
        silouhette = []
        #init index list
        index_ = []
        for i in K:
            print("Step: {} running...".format(i))
            index_.append(i)
            #Clustering
            kmeans = KMeans(n_clusters=i, random_state=random_s).fit(x_scaled)
            #Compute silhouette coef
            silhouette_avg = silhouette_score(data, kmeans.labels_)
            silouhette.append(silhouette_avg)
            print("Silhouette score for step", i, "=", silhouette_avg)
        #Creation of a df
        silouhette_coef = pd.DataFrame(silouhette, index=index_, columns=['coef'])
        silouhette_coef = silouhette_coef.sort_values(by='coef', ascending=False).reset_index()
        #Plot coef in function of the clusters
        ax = plt.figure().gca()
        ax.scatter(silouhette_coef['index'], silouhette_coef['coef'])
        plt.xlabel('Number of Clusters')
        plt.ylabel('Value')
        plt.title('Silhouette coefficient')
        ax.xaxis.set_major_locator(plt.MaxNLocator(integer=True))
        plt.show()
        return silouhette_coef

    def makeClustering(data, x_scaled, coef, random_s=42, algo='kmeans', epsilon=0.5):
        """Clustering function, user can choose the algo that He wants. \n
        Available algo : Kmeans, Hierarchical (in build), Db_scan
        Here, coef represents the number of clusters for kmeans (or df generated by silhouette's
        function) but also the min_sample for db_scan -- This will be changed in the future for
        better optimization. Only one algo at a time."""
        #Random seed fix
        np.random.seed(random_s)
        #Check if the input user on algo variable is in our list
        algos = ['kmeans', 'hierarchical', 'db_scan']
        if algo not in algos:
            raise ValueError("Algorithme pas bon. Voici la liste dispo: %s" % algos)
        #if kmeans :
        if algo == 'kmeans':
            try:
                clust = coef.iloc[0, 0] #If nbr_clust comes from the silhouette's function
            except AttributeError:
                clust = coef #if it's from user's input
            data_clust = clustering.kmeansClustering(data, x_scaled, clust, random_s)
        #If hierarchical clustering
        if algo == 'hierarchical':
            print('Still in build, use kmeans or db_scan for now')
        #If DB_SCAN
        if algo == 'db_scan':
            data_clust = clustering.db_scanClustering(data, x_scaled, random_s, epsilon, coef)
        return data_clust

    def grouping(data_clust):
        """This function only groups the dataframe, by mean. Usefull for the radar plot"""
        data_grouped = data_clust.groupby('Clusters').mean()
        return data_grouped

    def kmeansClustering(data, x_scaled, clust, random_s):
        """This function performs a clustering with kmeans algorithm. \n
        Warning : This function will use all of your CPU hearts by default."""
        np.random.seed(random_s)
        #Performs clustering with the right number of clusters
        kmeans = KMeans(n_clusters=clust, random_state=random_s, n_jobs=-1).fit(x_scaled)
        kmeans = pd.DataFrame(kmeans.labels_, index=data.index, columns=["Clusters"])
        #Merge on our main dataframe for better vizualisation of the clusters
        data_clust = pd.merge(data, kmeans, left_index=True, right_index=True, how='left')
        return data_clust

    def db_scanClustering(data, x_scaled, random_s, epsilon, mini_sample):
        """This function performs a clustering with kmeans algorithm
        Warning : This function will use all of your CPU hearts by default."""
        np.random.seed(random_s)
        #Train the algo
        db = DBSCAN(eps=epsilon, min_samples=mini_sample, n_jobs=-1).fit(x_scaled)
        #Get each cluster
        labels = pd.DataFrame(db.labels_, index=data.index, columns=["Clusters"])
        #Merge on main df
        data_clust = pd.merge(data, labels, left_index=True, right_index=True, how='left')
        return data_clust

# # Class Plot
class plotClustering:
    """Class to plot the results"""
    def plotPairplot(data_clust, save=False, *args):
        """Function to plot the pairplot"""
        warnings.filterwarnings("ignore") #Ignore warnings
        #Init
        sns.pairplot(data_clust, hue="Clusters")
        if save == True:
            try:
                plt.savefig(args + "Pairplot.png")
            except NameError:
                print('Missing the path for saving')
        plt.show()

    def plotBoxplot(data_clust, save=False, *args):
        """Function to plot the boxplot"""
        sous_echantillon = data_clust.copy()
        modalites = sous_echantillon["Clusters"].unique()
        for var in data_clust.columns:
            X = "Clusters" # qualitative
            Y = var # quantitative
            groupes = []
            for m in modalites:
                groupes.append(sous_echantillon[sous_echantillon[X] == m][Y].dropna())
            medianprops = {'color':"black"}
            meanprops = {'marker':'o', 'markeredgecolor':'black',
                         'markerfacecolor':'firebrick'}
            plt.figure(figsize=[10, 10])
            plt.boxplot(groupes, labels=modalites, showfliers=False, medianprops=medianprops,
                        vert=False, patch_artist=True, showmeans=True, meanprops=meanprops)
            plt.title("Boxplot")
            plt.xlabel(var)
            plt.ylabel("Clusters")
            if save == True:
                try:
                    plt.savefig(args + "boxplot" + var + ".png")
                except NameError:
                    print('Missing the path for saving')
            #display the plot
            plt.show()
   
    def plotRadarPlot(data_grouped, save=False, *args):
        """Function to plot the complex radar plot. \n
        Most of the code was found here : \n
        https://bit.ly/35Jgt31 \n
        But I modified some parts to automatize more and adapt it to my set of functions"""
        #We get the name of features
        variables = data_grouped.columns
        #We get the ranges of each features
        ranges = findRanges(data_grouped)
        #We plot each cluster on a different radar (better for vizualisation
        for i in range(0, len(data_grouped)):
            #Init the figure
            fig1 = plt.figure(figsize=(6, 6))
            #Init the radar
            radar = ComplexRadar(fig1, variables, ranges)
            #Init values on the radar
            radar.plot(data_grouped.loc[i, :], ranges)
            #Fill the radar (plot looks better with that fill)
            radar.fill(data_grouped.loc[i, :], alpha=0.2)
            if save == True:
                try:
                    plt.savefig(args + "radar" + data_grouped.loc[i, :] + ".png")
                except NameError:
                    print('Missing the path for saving')
            plt.show()

def findRanges(data_grouped):
    """Function to find the ranges of variables
    (used for radar plot only)"""
    ranges = []
    for i in data_grouped.columns:
        theRange = (data_grouped[i].min(), data_grouped[i].max())
        ranges.append(theRange)
    return ranges

def _invert(x, limits):
    """inverts a value x on a scale from
    limits[0] to limits[1]"""
    return limits[1] - (x - limits[0])

def _scale_data(data_grouped, ranges):
    """scales data[1:] to ranges[0],
    inverts if the scale is reversed"""
    x1, x2 = ranges[0]
    d = data_grouped[0]
    if x1 > x2:
        d = _invert(d, (x1, x2))
        x1, x2 = x2, x1
    sdata = [d]
    for d, (y1, y2) in zip(data_grouped[1:], ranges[1:]):
        if y1 > y2:
            d = _invert(d, (y1, y2))
            y1, y2 = y2, y1
        sdata.append((d-y1) / (y2-y1) * (x2 - x1) + x1)
    return sdata

def set_rgrids(self, radii, labels=None, angle=None, fmt=None,
               **kwargs):
    """Set the radial locations and labels of the *r* grids.
    The labels will appear at radial distances *radii* at the
    given *angle* in degrees.
    *labels*, if not None, is a ``len(radii)`` list of strings of the
    labels to use at each radius.
    If *labels* is None, the built-in formatter will be used.
    Return value is a list of tuples (*line*, *label*), where
    *line* is :class:`~matplotlib.lines.Line2D` instances and the
    *label* is :class:`~matplotlib.text.Text` instances.
    kwargs are optional text properties for the labels:
    %(Text)s
    ACCEPTS: sequence of floats"""
    # Make sure we take into account unitized data
    radii = self.convert_xunits(radii)
    radii = np.asarray(radii)
    self.set_yticks(radii)
    if labels is not None:
        self.set_yticklabels(labels)
    elif fmt is not None:
        self.yaxis.set_major_formatter(FormatStrFormatter(fmt))
    if angle is None:
        angle = self.get_rlabel_position()
    self.set_rlabel_position(angle)
    for t in self.yaxis.get_ticklabels():
        t.update(kwargs)
    return self.yaxis.get_gridlines(), self.yaxis.get_ticklabels()

class ComplexRadar:
    """This class is there to create the radar plot. \n
    Used for plotRadarplot but you can use it alone"""
    def __init__(self, fig, variables, ranges, n_ordinate_levels=6):
        """Init the radar plot"""
        angles = np.arange(0, 360, 360./len(variables))
        axes = [fig.add_axes([0.1, 0.1, 0.9, 0.9], polar=True,
                             label="axes{}".format(i)) for i in range(len(variables))]
        for ax in axes[1:]:
            ax.patch.set_visible(False)
            ax.grid("off")
            ax.xaxis.set_visible(False)
        for i, ax in enumerate(axes):
            grid = np.linspace(*ranges[i], num=n_ordinate_levels)
            gridlabel = ["{}".format(round(x, 2)) for x in grid]
            if ranges[i][0] > ranges[i][1]:
                grid = grid[::-1] # hack to invert grid
            gridlabel[0] = "" # clean up origin
            set_rgrids(ax, grid, labels=gridlabel, angle=angles[i])
            ax.set_ylim(*ranges[i])
        # variables for plotting
        self.angle = np.deg2rad(np.r_[angles, angles[0]])
        self.ranges = ranges
        self.ax = axes[0]

    def plot(self, data_grouped, *args, **kw):
        """Add the data to the radar"""
        sdata = _scale_data(data_grouped, self.ranges)
        self.ax.plot(self.angle, np.r_[sdata, sdata[0]], *args, **kw)

    def fill(self, data_grouped, *args, **kw):
        """Fill the radar plot"""
        sdata = _scale_data(data_grouped, self.ranges)
        self.ax.fill(self.angle, np.r_[sdata, sdata[0]], *args, **kw)