## -------------------------------------------------------------------------------------------------
## -- Project : MLPro - The integrative middleware framework for standardized machine learning
## -- Package : mlpro_int_river.wrappers.clusteranalyzers
## -- Module  : kmeans.py
## -------------------------------------------------------------------------------------------------
## -- History :
## -- yyyy-mm-dd  Ver.      Auth.    Description
## -- 2023-05-12  0.0.0     DA       Creation
## -- 2023-05-23  1.0.0     SY       First version release
## -- 2023-05-25  1.0.1     SY       Refactoring related to ClusterCentroid
## -- 2023-06-03  1.0.2     DA       Renaming of method ClusterAnalyzer.get_cluster_memberships
## -- 2023-06-05  1.0.3     SY       Updating get_cluster_memberships, p_cls_cluster, and _adapt
## -- 2023-11-20  1.0.4     SY       Update due to intorduction of visualization for ClusterCentroid
## -- 2023-12-08  1.0.5     SY       Update due to new data type in ClusterCentroid
## -- 2023-12-15  1.0.6     SY/DA    Adjustments on classes WrClusterAnalyzerRiver2MLPro and 
## --                                WrRiverClustStream2MLPro
## -- 2023-12-17  1.0.7     SY       Updates on classes WrRiverKMeans2MLPro, WrRiverDBStream2MLPro
## --                                WrRiverStreamKMeans2MLPro, WrRiverDenStream2MLPro
## -- 2023-12-21  1.0.8     SY       Add renormalization method on all wrapped algorithms
## -- 2023-12-22  1.0.9     DA/SY    Bugfix 
## -- 2023-12-25  1.0.10    DA       Bugfix in WrClusterAnalyzerRiver2MLPro._adapt()
## -- 2023-12-29  1.0.11    DA/SY    Disabled renormalization of CluStream, DBStream, DenStream
## -- 2024-01-05  1.0.12    DA       All classed: refactoring of naming (C_NAME)
## -- 2024-02-04  1.1.0     SY       Updating WrRiverDenStream2MLPro, WrRiverStreamKMeans2MLPro due
## --                                to visualization errors
## -- 2024-02-24  1.1.1     DA       Class WrClusterAnalyzerRiver2MLPro: package constants removed
## -- 2024-04-29  1.1.2     SY       Updating WrRiverDenStream2MLPro due to River 0.21.1
## -- 2024-04-30  1.2.0     DA       Alignment with MLPro 2
## -- 2024-05-05  1.3.0     DA       Alignment with MLPro 2
## -- 2024-05-07  1.4.0     DA       Separated to own module
## -------------------------------------------------------------------------------------------------

"""
Ver. 1.4.0 (2024-05-07)

This module provides a wrapper class for the KMeans algorithm provided by River.

Learn more:
https://www.riverml.xyz/
https://riverml.xyz/latest/api/cluster/KMeans/

"""


from mlpro_int_river.wrappers.clusteranalyzers.basics import WrClusterAnalyzerRiver2MLPro
from mlpro.bf.math.normalizers import Normalizer
from mlpro.oa.streams.tasks.clusteranalyzers.clusters import Cluster, ClusterCentroid
from mlpro.oa.streams.tasks.clusteranalyzers.clusters.properties import *
from mlpro.bf.mt import Task as MLTask
from mlpro.bf.various import Log
from mlpro.bf.streams import *
from river import cluster




## -------------------------------------------------------------------------------------------------
## -------------------------------------------------------------------------------------------------
class WrRiverKMeans2MLPro (WrClusterAnalyzerRiver2MLPro):
    """
    This is the wrapper class for KMeans clusterer.
    
    According to https://riverml.xyz/latest/api/cluster/KMeans/ :
    Incremental k-means. The most common way to implement batch k-means is to use Lloyd's algorithm,
    which consists in assigning all the data points to a set of cluster centers and then moving the
    centers accordingly.

    Parameters
    ----------
    p_name : str
        Name of the clusterer. Default: None.
    p_range_max :
        MLPro machine learning task, either process or thread. Default: MLTask.C_RANGE_THREAD.
    p_ada : bool
        Turn on adaptivity. Default: True.
    p_visualize : bool
        Turn on visualization. Default: False.
    p_logging :
        Set up type of logging. Default: Log.C_LOG_ALL.
    p_n_clusters : int
        Maximum number of clusters to assign. Default: 5.
    p_seed : int
        Random seed used for generating initial centroid positions. Default: None.
    p_halflife : float
        Amount by which to move the cluster centers, a reasonable value if between 0 and 1.
        Default: 0.5.
    p_mu : float
        Mean of the normal distribution used to instantiate cluster positions. Default: 1.
    p_sigma : float
        Standard deviation of the normal distribution used to instantiate cluster positions.
        Default: 1.
    p_p : int
        Power parameter for the Minkowski metric. When p=1, this corresponds to the Manhattan
        distance, while p=2 corresponds to the Euclidean distance. Default: 2.
    p_kwargs : dict
        Further optional named parameters. 
        
    """

    C_TYPE          = 'River Cluster Analyzer KMeans'


## -------------------------------------------------------------------------------------------------
    def __init__(self,
                 p_name:str = None,
                 p_range_max = MLTask.C_RANGE_THREAD,
                 p_ada:bool = True,
                 p_visualize:bool = False,
                 p_logging = Log.C_LOG_ALL,
                 p_n_clusters:int = 5,
                 p_halflife:float = 0.5,
                 p_mu:float = 0,
                 p_sigma:float = 1,
                 p_p:int = 2,
                 p_seed:int = None,
                 **p_kwargs):
        
        alg = cluster.KMeans(n_clusters=p_n_clusters,
                             halflife=p_halflife,
                             mu=p_mu,
                             sigma=p_sigma,
                             p=p_p,
                             seed=p_seed)

        super().__init__(p_cls_cluster=ClusterCentroid,
                         p_river_algo=alg,
                         p_name=p_name,
                         p_range_max=p_range_max,
                         p_ada=p_ada,
                         p_visualize=p_visualize,
                         p_logging=p_logging,
                         **p_kwargs)


## -------------------------------------------------------------------------------------------------
    def _update_clusters(self):
        """
        This method is to update the centroids of each introduced cluster.
        """
        for x in self._river_algo.centers.keys():
            related_cluster = self._clusters[x]
            list_center = []
            for y in range(len(self._river_algo.centers[x])):
                list_center.append(self._river_algo.centers[x][y+1])
            related_cluster.centroid.value = list_center


## -------------------------------------------------------------------------------------------------
    def get_clusters(self) -> dict[Cluster]:
        """
        This method returns the current list of clusters.

        Returns
        -------
        dict_of_clusters : dict[Cluster]
            Current list of clusters.
            
        """

        for x in self._river_algo.centers.keys():
            try:
                related_cluster = self._clusters[x]
            except:
                related_cluster = self._cls_cluster(p_id=x, p_properties=self.C_CLUSTER_PROPERTIES, p_visualize=self.get_visualization())  

                if self.get_visualization(): 
                    related_cluster.init_plot(p_figure = self._figure, p_plot_settings=self._plot_settings)  

                list_center = []
                for y in range(len(self._river_algo.centers[x])):
                    list_center.append(self._river_algo.centers[x][y+1])           
                related_cluster.centroid.value = list_center
                self._add_cluster( p_cluster = related_cluster )

        return self._clusters


## -------------------------------------------------------------------------------------------------
    def _renormalize(self, p_normalizer:Normalizer):
        """
        Internal renormalization of all clusters. See method OATask.renormalize_on_event() for further
        information.

        Parameters
        ----------
        p_normalizer : Normalizer
            Normalizer object to be applied on task-specific 
        """
        
        super()._renormalize(p_normalizer)

        for cluster in self._clusters.values():
            related_cluster = self._river_algo.centers[cluster.get_id()]
            for mlpro_idx, river_idx in enumerate(related_cluster):
                related_cluster[river_idx] = cluster.centroid.value[mlpro_idx]
