from sklearn import preprocessing
from sklearn.metrics import accuracy_score, precision_score, cohen_kappa_score, f1_score
import numpy as np
import os
import matplotlib.pyplot as plt

__all__ = [
    'MinMax',
    'ParameterDefinition',
    'OptimizationStats',
    'Factory',
    'get_bin_index'
]

def get_bin_index(value, number_of_bins):
    """Gets index of value's bin. Value must be between 0.0 and 1.0.

    Arguments:
        value (float): Value to put into bin.
        number_of_bins (uint): Number of bins on the interval [0.0, 1.0].
    
    Returns:
        uint: Calculated index.
    """
    bin_index = np.int(np.floor(value / (1.0 / number_of_bins)))
    if bin_index >= number_of_bins:
        bin_index -= 1
    return bin_index

class MinMax:
    r"""Class for ParameterDefinition's value property.
    
    Date:
        2020

    Author
        Luka Pečnik

    License:
        MIT

    Attributes:
        min (float): Minimum number (inclusive).
        max (float): Maximum number (exclusive).

    See Also:
        * :class:`niaaml.utilities.ParameterDefinition`
    """

    def __init__(self, min, max):
        r"""Initialize instance.

        Arguments:
            min (float): Minimum number (inclusive).
            max (float): Maximum number (exclusive).
        """
        self.min = min
        self.max = max

class ParameterDefinition:
    r"""Class for PipelineComponent parameters definition.
    
    Date:
        2020

    Author
        Luka Pečnik

    License:
        MIT

    Attributes:
        value (any): Array of possible parameter values or instance of MinMax class.
        param_type (numpy.dtype): Selection output data type.

    See Also:
        * :class:`niaaml.pipeline_component.PipelineComponent`
        * :class:`niaaml.utilities.MinMax`
    """

    def __init__(self, value, param_type = None):
        r"""Initialize instance.

        Arguments:
            value (Any): Array of any type or instance of MinMax class.
            param_type (Optional[type]): Type of possible outcome according to an instance of MinMax class. Not used if the argument value is array.
        """
        self.value = value
        self.param_type = param_type

class Factory:
    r"""Base class with string mappings to entities.
    
    Date:
        2020

    Author
        Luka Pečnik

    License:
        MIT

    Attributes:
        _entities (Dict[str, any]): Dictionary to map from strings to an instance of anything.
    """
    _entities = None

    def __init__(self, **kwargs):
        r"""Initialize the factory."""
        self._set_parameters(**kwargs)
    
    def _set_parameters(self, **kwargs):
        r"""Set the parameters/arguments of the factory.
        """
        return
    
    def get_result(self, name):
        r"""Get the resulting entity.

        Arguments:
            name (str): String that represents the entity.

        Returns:
            any: Entity according to the given name.
        """

        if name in self._entities:
            return self._entities[name]()
        else:
            raise TypeError('Passed entity is not defined! --> %s' % name)
    
    def get_name_to_classname_mapping(self):
        r"""Get dictionary of user-friendly name to class name mapping.

        Returns:
            dict: Dictionary of user-friendly name to class name mapping.
        """
        d = {}
        for k in self._entities:
            d[self._entities[k].Name] = k
        return d

class OptimizationStats:
    r"""Class that holds pipeline optimization result's statistics. Includes accuracy, precision, Cohen's kappa and F1-score.
    
    Date:
        2020

    Author
        Luka Pečnik

    License:
        MIT

    Attributes:
        _accuracy (float): Calculated accuracy.
        _precision (float): Calculated precision.
        _cohen_kappa (float): Calculated Cohen's kappa.
        _f1_score (float): Calculated F1-score.
        _fitness_function_values (numpy.array[float]): Array of fitness function's values in the evaluation process (10-fold cross validation's results).
        _fitness_function_name (str): Name of the used fitness function.
    """

    def __init__(self, predicted, expected, fitness_function_values, fitness_function_name, **kwargs):
        r"""Initialize the factory.

        Arguments:
            predicted (Iterable[any]): Array of predicted classes.
            expected (Iterable[any]): Array of expected classes.
            fitness_function_values (numpy.array[float]): Array of fitness function's values in the evaluation process (10-fold cross validation's results).
            fitness_function_name (str): Name of the used fitness function.
        """
        self._accuracy = accuracy_score(expected, predicted)
        self._precision = precision_score(expected, predicted, average='weighted')
        self._cohen_kappa = cohen_kappa_score(expected, predicted)
        self._f1_score = f1_score(expected, predicted, average='weighted')
        self._fitness_function_values = fitness_function_values
        self._fitness_function_name = fitness_function_name
    
    def export_boxplot(self, file_name):
        r"""Export boxplot of fitness function's values.

        Arguments:
            file_name (str): Output file name.
        """
        if len(os.path.splitext(file_name)[1]) == 0 or os.path.splitext(file_name)[1] != '.png':
            file_name = file_name + '.png'

        fig, ax = plt.subplots()
        ax.set_title(self._fitness_function_name)
        ax.boxplot(self._fitness_function_values)
        ax.tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False)
        plt.savefig(file_name)

    def to_string(self):
        r"""User friendly representation of the object.

        Returns:
            str: User friendly representation of the object.
        """
        return 'Accuracy: {acc},\nPrecision: {prc},\nCohen\'s kappa: {ck},\nF1-score: {f1},\n\nFitness function\'s ({fn}) 10-fold cross validation results: {arr}'.format(acc=self._accuracy, prc=self._precision, ck=self._cohen_kappa, f1=self._f1_score, fn=self._fitness_function_name, arr=np.array2string(self._fitness_function_values, separator=', '))