"""
This module contains APIs for creating the data entry with different criteria

APIs 
* create_cluster_representatives : create a data entry with ramdom member to be used as cluster medoids
    * parameters
        - number_of_cluster : (int) an integer number specifies number of cluster to create
        - size_of_set : (tuple(int,int)) a tuple of intergers specifies the minimum and maximum feature that each data has to contain
        - all_features : (string[]) an array of string containing all possible features of the dataset
    * return
        - all_representative : list of numpy array (cluster representative / medoids)

* generate_cluster_members : create data entries for all cluster along with its ground truths labels 
    * parameters
        - data_size : (int) an integer number specifies number of total number of data that will be generated
        - representatives : (list(numpy array)) list of numpy array (cluster representative / medoids)
        - size_of_set : (tuple(int,int)) a tuple of intergers specifies the minimum and maximum feature that each data has to contain
        - all_features : (string[]) an array of string containing all possible features of the dataset
        - distance_threshold : (float) a number specifies the maximum distance away from the cluster representative according to Jaccard's method
    * return
        type : tuple
        - artificially generated data set as a list of numpy array containing strings
        - ground truths labels  of the data set
"""

import numpy as np
import random
import math
from distances import jaccard

DEFAULT_STRING = '   '

def _create_cluster_member_random(all_features, min_feature, max_feature):
    number_of_member_features = random.randrange(min_feature, max_feature + 1)
    return random.sample(all_features.tolist(), number_of_member_features)

def _find_number_of_member_per_cluster(data_size, representatives):
    n_centers = len(representatives)
    number_of_data_per_cluster = [int((data_size) // n_centers)] * n_centers

    for i in range(data_size % n_centers):
        number_of_data_per_cluster[i] += 1

    return number_of_data_per_cluster

# [Unused]
def _create_cluster_member_with_one_different(representative, all_features):
    cp_representative = np.copy(representative)
    feature_id_to_change = random.randint(1, len(representative) - 1)
    new_feature = random.sample(all_features.tolist(), 1)[0]

    cp_representative[feature_id_to_change] = new_feature

    return cp_representative

# [Unused]
def _create_cluster_member_random_but_same_feature_length(representative, all_features):
    cp_representative = np.copy(representative)

    # How many number of ones in each sequence
    selected_features = np.random.randint(0, 2, size=len(representative))
    feature_ids_to_change = np.where(selected_features == 1)[0]

    if feature_ids_to_change.shape[0] == 0:
        return _create_cluster_member_with_one_different(representative, all_features)
    else:
        new_features = random.sample(all_features.tolist(), feature_ids_to_change.shape[0])[0]
        cp_representative[feature_ids_to_change] = new_features

        return cp_representative

def _create_cluster_member_random_different_feature_length(
    representative, 
    all_features, 
    min_feature, 
    max_feature,
    distance_threshold):
    similarity_threshold = 1 - distance_threshold

    number_of_member_features = random.randrange(min_feature, max_feature + 1)
    min_intersect = math.floor((similarity_threshold * (len(representative) + number_of_member_features)) / (1 + similarity_threshold))

    while(min_intersect >= number_of_member_features or min_intersect >= len(representative)):
        number_of_member_features = random.randrange(min_feature, max_feature + 1)
        min_intersect = math.floor((similarity_threshold * (len(representative) + number_of_member_features)) / (1 + similarity_threshold))

    new_member = np.full(number_of_member_features, DEFAULT_STRING)
    number_of_intersect = np.random.randint(min_intersect, min(number_of_member_features, len(representative)))
    total_available_features = [x for x in all_features if x not in representative]
    
    if number_of_member_features > len(representative):
        new_member[0:len(representative)] = representative
        id_to_change = random.sample(range(len(representative)), len(representative) - number_of_intersect)
        empty_space = len(np.where(new_member == DEFAULT_STRING)[0])
        new_features_pool = random.sample(total_available_features, len(id_to_change) + empty_space)
        new_member[len(representative):] = new_features_pool[0:(number_of_member_features - len(representative))]
        new_member[id_to_change] = new_features_pool[(number_of_member_features - len(representative)):]
    elif number_of_member_features < len(representative):
        selected_id = random.sample(range(len(representative)), number_of_member_features)
        new_member = representative[selected_id]
        new_features = random.sample(total_available_features, len(new_member) - number_of_intersect)
        id_to_change = random.sample(range(len(new_member)), len(new_member) - number_of_intersect)
        new_member[id_to_change] = new_features
    else:
        new_member = np.copy(representative)
        id_to_change = random.sample(range(number_of_member_features), number_of_member_features - number_of_intersect)
        new_features = random.sample(total_available_features, len(id_to_change))
        new_member[id_to_change] = new_features
    return new_member

def create_cluster_representatives(number_of_cluster, size_of_set, all_features):    
    all_representative = []
    for i in range(number_of_cluster):
        representative = np.array(_create_cluster_member_random(all_features, size_of_set[0], size_of_set[1]))
        all_representative.append(representative)

    return all_representative

def generate_cluster_members(
    data_size, 
    representatives, 
    size_of_set,
    all_features,
    distance_threshold,
    size_of_clusters):

    if len(size_of_clusters) == 0:
        number_of_data_per_cluster = _find_number_of_member_per_cluster(data_size, representatives)
    else : number_of_data_per_cluster = size_of_clusters

    data = []
    ground_truth_labels = []

    for i in range(len(representatives)):
        cluster_len = 0

        data.append(representatives[i])
        ground_truth_labels.append(i)

        cluster_len = 1

        while cluster_len != number_of_data_per_cluster[i]:
            member = _create_cluster_member_random_different_feature_length(
                representatives[i], 
                all_features, 
                size_of_set[0], 
                size_of_set[1], 
                distance_threshold)

            if jaccard.jaccard_seq(representatives[i], member) < distance_threshold:
                data.append(member)
                ground_truth_labels.append(i)
                cluster_len = cluster_len + 1

    if len(np.array(ground_truth_labels)) != len(data):
        raise Exception('Program terminated, lengths of data and ground truths labels are not equal')

    return (data, np.array(ground_truth_labels))