from __future__ import annotations

import json
from datetime import datetime

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split


class DatasetPreprocessing():
    SENSORCONFIG_FILE = 'models/config/sensors.json'
    CATEGORICAL_KEY = 'categorical'
    NUMERICAL_KEY = 'numerical'

    @classmethod
    def sensorsconfig(cls):
        categoricals = []
        numerical = []

        with open(cls.SENSORCONFIG_FILE, 'r', encoding='utf-8') as json_file:
            sensors = json.load(json_file)
            categoricals = list(map(lambda x: x[0], filter(
                lambda x: x[1] == cls.CATEGORICAL_KEY, sensors.items())))
            numerical = list(map(lambda x: x[0], filter(
                lambda x: x[1] != cls.NUMERICAL_KEY, sensors.items())))

        categoricals.append('month_of_year')
        numerical.append('day_of_month')
        categoricals.append('day_of_week')
        numerical.append('hour_of_day')
        numerical.append('minute_of_hour')

        return categoricals, numerical

    @staticmethod
    def add_time_information(data):
        if 'timestamp' not in data:
            data['timestamp'] = datetime.now().timestamp()

        date = datetime.fromtimestamp(data['timestamp'])
        data['datetime'] = date.strftime('%Y-%m-%d %H:%M:%s')
        data['month_of_year'] = date.month
        data['day_of_month'] = date.day
        data['day_of_week'] = date.strftime('%A')
        data['hour_of_day'] = date.hour
        data['minute_of_hour'] = date.minute

        return data

    @classmethod
    def get_x_selected_and_numerical_columns(cls, modelcfg, data):
        categoricals, numericals = cls.sensorsconfig()

        categoricals = cls.columns_intersection(categoricals, data)

        if len(categoricals) > 0:
            used_columns = categoricals + \
                cls.columns_intersection(numericals, data)

            x_temp = pd.get_dummies(data[used_columns], columns=categoricals)

            features_in_dataframe = cls.columns_intersection(
                x_temp, modelcfg.features)

            x_selected = x_temp[features_in_dataframe]
        else:
            features_in_dataframe = cls.columns_intersection(
                data, modelcfg.features)

            x_selected = data[features_in_dataframe]

        return x_selected, numericals

    @classmethod
    def prepare_training(cls, modelcfg, data: pd.DataFrame):
        x_vector, numericals = cls.get_x_selected_and_numerical_columns(
            modelcfg, data)

        y_vector = data[modelcfg.dependent]

        if modelcfg.dependent_encode:
            y_vector = modelcfg.dependent_encoder.fit_transform(y_vector)

        x_train, x_test, y_train, y_test = train_test_split(
            x_vector, y_vector, test_size=modelcfg.testsize, random_state=0)

        numericals = cls.columns_intersection(
            numericals, modelcfg.features)

        x_train = DatasetPreprocessing.transform_columns(
            modelcfg.imputer.fit_transform, x_train, numericals)
        x_test = DatasetPreprocessing.transform_columns(
            modelcfg.imputer.transform, x_test, numericals)

        if modelcfg.has_standard_scaled():
            if not modelcfg.standard_scaler_fitted:
                x_train = DatasetPreprocessing.transform_columns(
                    modelcfg.standard_scaler.fit_transform, x_train, modelcfg.standard_scaled)
                modelcfg.standard_scaler_fitted = True

            x_test = DatasetPreprocessing.transform_columns(
                modelcfg.standard_scaler.transform, x_test, modelcfg.standard_scaled)

        return modelcfg, x_train, x_test, y_train, y_test

    @classmethod
    def prepare_prediction(cls, modelcfg, data):
        x_vector, numericals = cls.get_x_selected_and_numerical_columns(
            modelcfg, data)

        numericals = list(set.intersection(
            set(modelcfg.columns), set(numericals)))

        missing_columns = set.difference(cls.set_of_columns(
            numericals), cls.set_of_columns(x_vector))

        for missing_column in missing_columns:
            x_vector.insert(0, missing_column, [np.nan])

        x_vector = DatasetPreprocessing.transform_columns(
            modelcfg.imputer.transform, x_vector, numericals)

        x_vector = x_vector.reindex(columns=modelcfg.columns, fill_value=0)

        if modelcfg.has_standard_scaled():
            x_vector = DatasetPreprocessing.transform_columns(
                modelcfg.standard_scaler.transform, x_vector, modelcfg.standard_scaled)

        return x_vector

    @staticmethod
    def transform_columns(func, data, columns):
        data_temp = data.copy()
        data_temp[columns] = func(data[columns])
        return data_temp

    @classmethod
    def columns_intersection(cls, list_or_dataframe1, list_or_dataframe2):
        set1 = cls.set_of_columns(list_or_dataframe1)
        set2 = cls.set_of_columns(list_or_dataframe2)
        return list(set.intersection(set1, set2))

    @staticmethod
    def set_of_columns(list_or_dataframe):
        set_of_columns = None
        if isinstance(list_or_dataframe, pd.DataFrame):
            set_of_columns = set(list_or_dataframe.columns.values.tolist())
        elif isinstance(list_or_dataframe, list):
            set_of_columns = set(list_or_dataframe)

        return set_of_columns
