import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.spatial.distance import euclidean as dis
from scipy.cluster.hierarchy import dendrogram, linkage
import matplotlib.pyplot as plt


class DivisiveAnalysis:
    #Class which implements the DIANA (Divisive Analysis) algorithm and provides an interface for using the implementation.
    def __init__(self,no_clusters):
      self.no_clusters = no_clusters
      self.labels = []
      self.clusters = [0]
    
    def initializeToZero(self,X):
      # Given a dataframe
      # Makes a list of zeros which are the initial labels
      return [0 for i in range(X.shape[0])]

    def cluster_list(self,X,c):
      # Given a dataframe, their respective labels and a single label
      # Returns a list of points which have label 'c' and their respective index
      list = []
      for i,p in enumerate(X):
        if self.labels[i] == c:
          list.append([p[k] for k in range(len(p))] + [i])
      return np.array(list)

    def diameterOfCluster(self, x):
      # Given a dataframe
      # Returns the maximum distance between two points in it
      distances = [0] # Initialized with a zero for clusters of 1 point that won´t enter the loop below
      for i in range(x.shape[0]):
        for j in range(i+1,x.shape[0]):
          distances.append(dis(x[i],x[j]))
      return max(distances)


    def largest_cluster(self, X):
      # Given a dataframe, its labels, and a list of unique labels contained in labels
      # Returns the label of the biggest cluster (the one with the biggest diameter)
      max_val = 0
      max_cluster = 0
      for c in self.clusters:
        diam = self.diameterOfCluster(self.cluster_list(X,c))
        if diam > max_val:
          max_val = diam
          max_cluster = c
      return max_cluster
    
    def get_clusters(self):
      return self.clusters

    def The_average_point(self,p,x):
      # Given a point and a dataframe
      # Returns the mean distance between the point and each point in the dataframe
      point_in_cluster = False

      sum_distances = 0
      for point in x:
        for i in range(len(p)):
          if point[i] == p[i]:
            point_in_cluster = True
        sum_distances += dis(p,point)

      if point_in_cluster:
        if len(x) == 1:  
              return 0
        else:
          return sum_distances/(len(x) - 1)  # we subtract 1 p is part of x 
      else:
        return sum_distances/(len(x)) 

    def splinter(self,x_i):
      # Given a dataframe with (x,y,z...,i) where i is the index
      # Returns the index of the point in the dataframe which in average is furthest apart from the rest
      splinter = 0
      max_avg = 0
      
      for point in x_i:
        The_average = self.The_average_point(point[:-1],x_i[:,:-1])
        if The_average > max_avg:
          max_avg = The_average
          splinter = point[len(point)-1]
      return int(splinter)

    def a_splinter(self,p,U,D):
      # Given a point, the father dataframe and the splinter dataframe
      # Returns True if the point should be in the splinter dataframe using the average distance
      if self.The_average_point(p,D) < self.The_average_point(p,U):
        return True
      else:
        return False


    def create_dendrogram(self,X):
      #Function to create the dendrogram using the linkage matrix and save it to a file.
      fig = plt.figure(figsize=(18, 8))
      plt.title("Dendrogram - Divisive Clustering")
      Z = linkage(X, 'ward')
      dendrogram(Z, orientation='top', labels=self.labels)
      fig.savefig('DIANA\dendrograms\dendrogram.png')
      plt.show()

    def fit(self,X): 
      if X.shape[0] < self.no_clusters:
        import warnings
        warnings.warn("The number of clusters can't be larger than number of points in the dataframe")
        return None

      X = np.array(X)

      self.labels = self.initializeToZero(X)

      for z in range(self.no_clusters-1):

        # Get the largest cluster
        majorClusterLabel = self.largest_cluster(X)
        majorClusterList = self.cluster_list(X,majorClusterLabel)

        # Get the splinter that makes your new cluster
        disi = self.splinter(majorClusterList)
        self.clusters.append(self.clusters[-1]+1) 
        self.labels[disi] = self.clusters[-1]
        newClusterList = self.cluster_list(X,self.clusters[-1])
        m = newClusterList.copy()
        M = majorClusterList.copy()


        # pass the points to the splinter cluster
        changed = True
        max_changes = X.shape[0]*4
        changes = 0
        while changed == True and changes != max_changes:
          changed = False
          for i in range(len(majorClusterList)):
            if self.labels[int(majorClusterList[i][-1])] == majorClusterLabel:
              if self.a_splinter(majorClusterList[i][:-1],M[:,:-1],m[:,:-1]):
                changed = True
                self.labels[int(majorClusterList[i][-1])] = self.clusters[-1]
                M = self.cluster_list(X,majorClusterLabel)
                m = self.cluster_list(X,self.clusters[-1])
            else:
              if not self.a_splinter(majorClusterList[i][:-1],M[:,:-1],m[:,:-1]):
                changed = True
                self.labels[int(majorClusterList[i][-1])] = majorClusterLabel
                M = self.cluster_list(X,majorClusterLabel)
                m = self.cluster_list(X,self.clusters[-1])
                changes += 1

      return(list(self.labels))



if __name__ == '__main__':
    pass
