import json
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import OPTICS
from typing import List, Dict, Any, TypeVar

T = TypeVar('T')
ClusterGroup = Dict[Any, List[Dict]]

class Clusterer:
    '''
    Clusterer for applying density-based clustering on datasets.
    The clustering is done with OPTICS.

    :param epsilon: Eps used in OPTICS 
    :param min_points: MinPts used in OPTICS
    '''
    def __init__(self, min_points=5):
        self.min_points = min_points
    
    def create_labels(self, features:np.ndarray) -> List[int]:
        '''Creates labels for the items based on OPTICS.'''
        if features is None or len(features) == 0:
            return features # trash in trash out
        if len(features) == 1:
            return [-1]
    
        optics = OPTICS(min_samples=self.min_points)
        optics = optics.fit(features)
        labels = optics.labels_

        return labels.tolist()

    def _extract_features(self, dataset: List[Dict], features:List[str]) -> np.ndarray:
        '''Extracts the feature values from the dataset into a np array with same order as original dataset.'''
        extracted_features = []
        for data in dataset:
            entry = [float(data[feature]) for feature in features]
            extracted_features.append(entry)

        return np.asarray(extracted_features)

    def label_dataset(self, dataset:List[Dict], labels:List[Any]):
        '''Adds the labels to the elements of the dataset at the same position. The new key is called cluster_label.'''
        if dataset is None or labels is None:
            return

        if len(dataset) != len(labels):
            raise ValueError("dataset and labels has to have same length")

        for i in range(len(dataset)):
            if 'cluster_label' in dataset[i]:
                continue
            dataset[i]['cluster_label'] = labels[i]

    def group_by_clusters(self, dataset:List[Dict], labels:List[Any]) -> ClusterGroup:
        clusters = {}
        for label in labels:
            clusters[label] = [ds for ds in dataset if ds['cluster_label'] == label]
        
        return clusters

    def cluster_dataset(self, dataset:List[Dict], features:List[str]) -> ClusterGroup:
        '''
        Returns the identified clusters containing a subset of nodes from the dataset.
        
        :param dataset: The nodes to assign to clusters
        :param features: The feature names of the nodes to use for clustering

        :returns: A dictionary of clusters, where each value is a non-empty subset of dataset if dataset was not empty
        '''
        arr = self._extract_features(dataset, features)

        labels = self.create_labels(arr)
        
        self.label_dataset(dataset, labels)

        return self.group_by_clusters(dataset, labels)

