import os
import pandas as pd
import numpy as np

from  sdg_utils import Positive, Binary

import openpyxl

from sklearn.preprocessing import OneHotEncoder

from typing import Tuple, List

def prepare_ALZ_BALEA(dataset_path : str = "", filename : str = "") ->  Tuple[pd.DataFrame,
                                                        pd.DataFrame, pd.DataFrame, List, str] :
    """Read the Alzheimer-Balea dataset from a .xlsx file and suit it to be processed
    as a pd.DataFrame. It returns tha dataset dataframe and strings associated to
    it to easy its management.

    Args:
    -----
            dataset_path: path where ALZ-BALEA dataset is stored. Set by default.
            filename : file name of the .csv containing the dataset. Set by default.

    Returns:
    --------
            data: dataframe containing the whole dataset
            X : dataframe containing the dataset features
            Y : dataframe containing only the target variable
            cols_names: list of strings containing feature names.
            y_tag: string containing target variable name.
    """

    # Go to dataset path
    os.chdir(dataset_path)

    # Open the Excel file
    bd = openpyxl.load_workbook(filename)

    # Load the useful information of the Excel file
    sheet = bd['Tesis definitiva']

    # Convert sheet into DataFrame
    data = pd.DataFrame(sheet.values)

    # Fix data properly in the dataframe
    tags = data.iloc[0]
    data.columns = tags

    # Drop useless rows and columns
    data = data.drop(index = [0,86,87,88,89,90])
    data.reset_index(drop=True, inplace=True)
    data = data.drop(['Grupo','ID','Pfeiffer','Comentario', 'HiperEspectMIC', 'HiperEspectMAC'], axis=1)

    # Replace "NULL" values by a integer to be handled later
    data = data.replace(to_replace = "#NULL!", value = 99999)

    # Store features' and target variable's names
    cols_names_prev = data.columns
    y_tag = cols_names_prev[len(cols_names_prev)-1]
    cols_names = cols_names_prev[0:cols_names_prev.size]

    # Substitute value of 6 (non-existant) in AntNeurol by 99999, as if it was "NULL"
    data['AntNeurolog'][61] = 99999

    # Drop those rows which has the output 'TNeurolog' equals to 99999 (NULL)
    data = data[data['TNeurocog'] < 2]

    # Save X, Y, feature names and Y name
    y_tag = cols_names[len(cols_names)-1]
    cols_names = cols_names[0:len(cols_names)-1]
    X = data[cols_names]
    Y = data[y_tag]

    return data, X, Y, cols_names, y_tag

def numerical_conversion(data : np.array, features : str, y_col : str) -> Tuple[pd.DataFrame,
                                            pd.DataFrame, pd.DataFrame]:
    """Fix all Alzheimer-Balea database features data types to its original type after
    KNNImputer is used, since this functions returns only a floating points ndarray.
    For more, check sklearn documentation of this function at
    https://scikit-learn.org/stable/modules/generated/sklearn.impute.KNNImputer.html. After
    fixing datatypes, an ndarray to pd.DataFrame conversion is performed. Notice that this
    operation is only done in the fields that were not originally floats.

    Args:
    -----
            data: data returned by KNN Imputer (float data type).
            features: list of strings containing the feature names of the dataset.
            y_col: target variable (i.e., Y) name

    Returns:
    --------
            data: dataframe containing the whole dataset after imputation
            X : dataframe containing the dataset features after imputation
            y : dataframe containing only the target variable after imputation
    """
    # From ndarray to pd.DataFrame
    names = features.insert(len(features), y_col)
    data = pd.DataFrame(data, columns = names)

    # Fixing necessary datatypes to int (including categorical variables)
    data['Edad'] = data['Edad'].astype(int)
    data['Sexo'] = data['Sexo'].astype(int)
    data['EstCivil'] = data['EstCivil'].astype(int)
    data['ActLaboral'] = data['ActLaboral'].astype(int)
    data['FormatConvivencia'] = data['FormatConvivencia'].astype(int)
    data['NivelEducativo'] = data['NivelEducativo'].astype(int)
    data['ActIntelectual'] = data['ActIntelectual'].astype(int)
    data['RelSociales'] = data['RelSociales'].astype(int)
    data['AntPsiquia'] = data['AntPsiquia'].astype(int)
    data['AntCardiologi'] = data['AntCardiologi'].astype(int)
    data['AntNeurolog'] = data['AntNeurolog'].astype(int)
    data['AntRenal'] = data['AntRenal'].astype(int)
    data['AntPulmonar'] = data['AntPulmonar'].astype(int)
    data['AntDemencia'] = data['AntDemencia'].astype(int)
    data['Tabaco'] = data['Tabaco'].astype(int)
    data['Alcohol'] = data['Alcohol'].astype(int)
    data['HTAanterior'] = data['HTAanterior'].astype(int)
    data['TAS'] = data['TAS'].astype(int)
    data['TAD'] = data['TAD'].astype(int)
    data['DM'] = data['DM'].astype(int)
    data['Barthel'] = data['Barthel'].astype(int)
    data['Hb'] = data['Hb'].astype(float)
    data['VCM'] = data['VCM'].astype(float)
    data['HCM'] = data['HCM'].astype(float)
    data['Plaquetas'] = data['Plaquetas'].astype(int)
    data['Leucocitos'] = data['Leucocitos'].astype(float)
    data['VCM'] = data['VCM'].astype(float)
    data['Neutrófilos'] = data['Neutrófilos'].astype(float)
    data['Linfocitos'] = data['Linfocitos'].astype(float)
    data['Monocitos'] = data['Monocitos'].astype(float)
    data['Glucosa'] = data['Glucosa'].astype(int)
    data['Creatinina'] = data['Creatinina'].astype(float)
    data['FG'] = data['FG'].astype(float)
    data['Na'] = data['Na'].astype(int)
    data['K'] = data['K'].astype(float)
    data['ALT'] = data['ALT'].astype(float)
    data['Colesterol'] = data['Colesterol'].astype(int)
    data['LDL'] = data['LDL'].astype(int)
    data['TNeurocog'] = data['TNeurocog'].astype(int)

    # Separate X and Y
    X = data[features]
    y = data[[y_col]]

    return data, X, y

def general_conversion (data : pd.DataFrame) -> pd.DataFrame :
    """Fix all Alzheimer-Balea database features data types to its original type.
    Categorical variables are set as "object" type. A DataFrame with the original
    datatypes of this database is returned.

    Args:
    -----
            data: dataset with datatypes not corresponding to the original ones.
            features: list of strings containing the feature names of the dataset.
            y_col: target variable (i.e., Y) name

    Returns:
    --------
            data: dataframe with the original datatypes
    """
    data['Edad'] = data['Edad'].astype(int)
    data['Sexo'] = data['Sexo'].astype('object')
    data['EstCivil'] = data['EstCivil'].astype('object')
    data['ActLaboral'] = data['ActLaboral'].astype('object')
    data['FormatConvivencia'] = data['FormatConvivencia'].astype('object')
    data['NivelEducativo'] = data['NivelEducativo'].astype('object')
    data['ActIntelectual'] = data['ActIntelectual'].astype('object')
    data['RelSociales'] = data['RelSociales'].astype('object')
    data['AntPsiquia'] = data['AntPsiquia'].astype('object')
    data['AntNeurolog'] = data['AntNeurolog'].astype('object')
    data['AntRenal'] = data['AntRenal'].astype('object')
    data['AntPulmonar'] = data['AntPulmonar'].astype('object')
    data['AntDemencia'] = data['AntDemencia'].astype('object')
    data['Tabaco'] = data['Tabaco'].astype('object')
    data['Alcohol'] = data['Alcohol'].astype('object')
    data['AntPsiquia'] = data['AntPsiquia'].astype('object')
    data['AntCardiologi'] = data['AntCardiologi'].astype('object')
    data['HTAanterior'] = data['HTAanterior'].astype(int)
    data['TAS'] = data['TAS'].astype(int)
    data['TAD'] = data['TAD'].astype(int)
    data['DM'] = data['DM'].astype(int)
    data['Barthel'] = data['Barthel'].astype(int)
    data['Hb'] = data['Hb'].astype(float)
    data['VCM'] = data['VCM'].astype(float)
    data['HCM'] = data['HCM'].astype(float)
    data['Plaquetas'] = data['Plaquetas'].astype(int)
    data['Leucocitos'] = data['Leucocitos'].astype(float)
    data['VCM'] = data['VCM'].astype(float)
    data['Neutrófilos'] = data['Neutrófilos'].astype(float)
    data['Linfocitos'] = data['Linfocitos'].astype(float)
    data['Monocitos'] = data['Monocitos'].astype(float)
    data['Glucosa'] = data['Glucosa'].astype(int)
    data['Creatinina'] = data['Creatinina'].astype(float)
    data['FG'] = data['FG'].astype(float)
    data['Na'] = data['Na'].astype(int)
    data['K'] = data['K'].astype(float)
    data['ALT'] = data['ALT'].astype(float)
    data['Colesterol'] = data['Colesterol'].astype(int)
    data['LDL'] = data['LDL'].astype(int)
    data['TNeurocog'] = data['TNeurocog'].astype(int)

    return data

def replacement(data : pd.DataFrame) -> pd.DataFrame :
    """This function replaces the numerical values corresponding to categories in
    the Alzheimer-Balea database by its correspondant category. It returns a DataFrame
    after this replacement.

    Args:
    -----
            data: dataset with categories represented by numbers.

    Returns:
    --------
            data: dataframe with the categories represented by their correspondant string
    """
    data['Sexo']  = data['Sexo'].replace([1,2],['Varon', 'Mujer'])
    data['EstCivil']  = data['EstCivil'].replace([1,2,3,4],['Solteria', 'Matrimonio',
                                                                        'Viudedad', 'Divorcio'])
    data['ActLaboral']  = data['ActLaboral'].replace([1,2,3,4],['Cualificado', 'No cualificado',
                                                                        'Empresario', 'Ama de casa'])
    data['FormatConvivencia']  = data['FormatConvivencia'].replace([1,2,3,4],['Convivencia Conyuge', 'Convivencia Solo',
                                                                        'Convivencia Hijos_Familiares', 'Convivencia Residencia'])
    data['NivelEducativo']  = data['NivelEducativo'].replace([1,2,3,4],['Sin estudios', 'Estudios primarios',
                                                                        'Estudios medios', 'Estudios universitarios'])
    data['ActIntelectual']  = data['ActIntelectual'].replace([1,2,3],['+10 libros/año', '5-10 libros/año',
                                                                        'Lectura escasa'])
    data['RelSociales']  = data['RelSociales'].replace([1,2,3],['Relsociales Buenas', 'Relsociales Normales',
                                                                             'Relsociales Inexistentes'])
    data['AntPsiquia']  = data['AntPsiquia'].replace([1,2,3],['Psiquia Depresion', 'Psiquia Otros antecedentes', 'Psiquia Sin antecedentes'])
    data['AntCardiologi']  = data['AntCardiologi'].replace([1,2,3,4],['Cardio Isquemica', 'Cardio FA',
                                                                            'Cardio otras','Cardio Sin antecedentes'])
    data['AntNeurolog'] = data['AntNeurolog'].replace([1,2,3,4,5],['Neuro Ictus', 'Neuro Cefalea', 'Neuro Traumatismo',
                                                                            'Neuro Epilepsia','Neuro Sin antecedentes'])
    data['AntRenal'] = data['AntRenal'].replace([1,2,3],['Renal ERC', 'Renal Otras', 'Renal Sin Patologias'])
    data['AntPulmonar'] = data['AntPulmonar'].replace([1,2,3,4],['Pulmonar EPOC', 'Pulmonar Cancer',
                                                                             'Pulmonar Otras','Pulmonar Sin Patologias'])
    data['AntDemencia'] = data['AntDemencia'].replace([1,2,3],['Demencia Padres', 'Demencia otros',
                                                                             'Demencia Sin Antecedentes'])
    data['Tabaco'] = data['Tabaco'].replace([1,2],['Ex/Fumador', 'No Fumador'])
    data['Alcohol'] = data['Alcohol'].replace([1,2,3,4],['Alcohol Elevado', 'Alcohol Moderado',
                                                                           'Alcohol leve', 'Alcohol nunca'])
    data['HTAanterior'] = data['HTAanterior'].replace([1,2],[1,0])
    data['DM'] = data['DM'].replace([1,2],[1,0])

    return data

def one_hot_enc(data : pd.DataFrame) -> pd.DataFrame :
    """This function performs One-Hot Encoding in the Alzheimer-Balea database. Since this
    database is really small, validation and train sets are even smaller. Hence, sometimes
    columns full of 0s must be manually added, because a certain value of a feature does not
    appear in the dataset subset. This is the case of category 'Empresarios' of feature "ActLaboral".

    Args:
    -----
            data: dataset with categories represented by numbers.

    Returns:
    --------
            data: dataframe with the categories represented by their correspondant string
    """

    # One-hot Encoder declaration
    enc = OneHotEncoder(handle_unknown='ignore')
    # Sexo
    cats = ['Varon', 'Mujer']
    data[['Sexo']] = data[['Sexo']].astype('category')
    sexo = pd.DataFrame(enc.fit_transform(data[['Sexo']]).toarray())
    sexo.columns = enc.categories_
    for name in cats:
        if name not in sexo:
            sexo[name] = 0
    sexo = sexo[['Varon', 'Mujer']]
    sexo.reset_index(drop=True, inplace=True)
    # Estado Civil
    cats = ['Solteria', 'Matrimonio', 'Viudedad', 'Divorcio']
    data[['EstCivil']] = data[['EstCivil']].astype('category')
    civil = pd.DataFrame(enc.fit_transform(data[['EstCivil']]).toarray())
    civil.columns = enc.categories_
    for name in cats:
        if name not in civil:
            civil[name] = 0
    civil = civil[['Solteria', 'Matrimonio', 'Viudedad', 'Divorcio']]
    civil.reset_index(drop=True, inplace=True)
    # Actividad laboral
    cats = ['Cualificado', 'No cualificado','Empresario', 'Ama de casa']
    data[['ActLaboral']] = data[['ActLaboral']].astype('category')
    laboral = pd.DataFrame(enc.fit_transform(data[['ActLaboral']]).toarray())
    laboral.columns = enc.categories_
    for name in cats:
        if name not in laboral:
            laboral[name] = 0
    laboral = laboral[['Cualificado', 'No cualificado','Empresario', 'Ama de casa']]
    laboral.reset_index(drop=True, inplace=True)
    # Formato Convivencia
    cats = ['Convivencia Conyuge', 'Convivencia Solo','Convivencia Hijos_Familiares',
            'Convivencia Residencia']
    data[['FormatConvivencia']] = data[['FormatConvivencia']].astype('category')
    convi = pd.DataFrame(enc.fit_transform(data[['FormatConvivencia']]).toarray())
    convi.columns = enc.categories_
    for name in cats:
        if name not in convi:
            convi[name] = 0
    convi = convi[['Convivencia Conyuge', 'Convivencia Solo','Convivencia Hijos_Familiares',
            'Convivencia Residencia']]
    convi.reset_index(drop=True, inplace=True)
    # Nivel educativo
    cats = ['Sin estudios', 'Estudios primarios','Estudios medios', 'Estudios universitarios']
    data[['NivelEducativo']] = data[['NivelEducativo']].astype('category')
    educ = pd.DataFrame(enc.fit_transform(data[['NivelEducativo']]).toarray())
    educ.columns = enc.categories_
    for name in cats:
        if name not in educ:
            educ[name] = 0
    educ = educ[['Sin estudios', 'Estudios primarios','Estudios medios', 'Estudios universitarios']]
    educ.reset_index(drop=True, inplace=True)
    # Acitividad intelectual
    cats = ['+10 libros/año', '5-10 libros/año', 'Lectura escasa']
    data[['ActIntelectual']] = data[['ActIntelectual']].astype('category')
    intec = pd.DataFrame(enc.fit_transform(data[['ActIntelectual']]).toarray())
    intec.columns = enc.categories_
    for name in cats:
        if name not in intec:
            intec[name] = 0
    intec = intec[['+10 libros/año', '5-10 libros/año', 'Lectura escasa']]
    intec.reset_index(drop=True, inplace=True)
    # Relaciones sociales
    cats = ['Relsociales Buenas', 'Relsociales Normales','Relsociales Inexistentes']
    data[['RelSociales']] = data[['RelSociales']].astype('category')
    rels = pd.DataFrame(enc.fit_transform(data[['RelSociales']]).toarray())
    rels.columns = enc.categories_
    for name in cats:
        if name not in rels:
            rels[name] = 0
    rels = rels[['Relsociales Buenas', 'Relsociales Normales','Relsociales Inexistentes']]
    rels.reset_index(drop=True, inplace=True)
    # Psiquiatria
    cats = ['Psiquia Depresion', 'Psiquia Otros antecedentes', 'Psiquia Sin antecedentes']
    data[['AntPsiquia']] = data[['AntPsiquia']].astype('category')
    psiq = pd.DataFrame(enc.fit_transform(data[['AntPsiquia']]).toarray())
    psiq.columns = enc.categories_
    for name in cats:
        if name not in psiq:
            psiq[name] = 0
    psiq = psiq[['Psiquia Depresion', 'Psiquia Otros antecedentes', 'Psiquia Sin antecedentes']]
    psiq.reset_index(drop=True, inplace=True)
    # Cardiologia
    cats = ['Cardio Isquemica', 'Cardio FA','Cardio otras','Cardio Sin antecedentes']
    data[['AntCardiologi']] = data[['AntCardiologi']].astype('category')
    card = pd.DataFrame(enc.fit_transform(data[['AntCardiologi']]).toarray())
    card.columns = enc.categories_
    for name in cats:
        if name not in card:
            card[name] = 0
    card = card[['Cardio Isquemica', 'Cardio FA','Cardio otras','Cardio Sin antecedentes']]
    card.reset_index(drop=True, inplace=True)
    # Neurologia
    cats = ['Neuro Ictus', 'Neuro Cefalea', 'Neuro Traumatismo','Neuro Epilepsia','Neuro Sin antecedentes']
    data[['AntNeurolog']] = data[['AntNeurolog']].astype('category')
    neur = pd.DataFrame(enc.fit_transform(data[['AntNeurolog']]).toarray())
    neur.columns = enc.categories_
    for name in cats:
        if name not in neur:
            neur[name] = 0
    neur = neur[['Neuro Ictus', 'Neuro Cefalea', 'Neuro Traumatismo','Neuro Epilepsia',
                 'Neuro Sin antecedentes']]
    neur.reset_index(drop=True, inplace=True)
    # Renal
    cats = ['Renal ERC', 'Renal Otras', 'Renal Sin Patologias']
    data[['AntRenal']] = data[['AntRenal']].astype('category')
    ren = pd.DataFrame(enc.fit_transform(data[['AntRenal']]).toarray())
    ren.columns = enc.categories_
    for name in cats:
        if name not in ren:
            ren[name] = 0
    ren = ren[['Renal ERC', 'Renal Otras', 'Renal Sin Patologias']]
    ren.reset_index(drop=True, inplace=True)
    # Pulmonar
    cats = ['Pulmonar EPOC', 'Pulmonar Cancer','Pulmonar Otras','Pulmonar Sin Patologias']
    data[['AntRenal']] = data[['AntRenal']].astype('category')
    pulm = pd.DataFrame(enc.fit_transform(data[['AntPulmonar']]).toarray())
    pulm.columns = enc.categories_
    for name in cats:
        if name not in pulm:
            pulm[name] = 0
    pulm = pulm[['Pulmonar EPOC', 'Pulmonar Cancer','Pulmonar Otras','Pulmonar Sin Patologias']]
    pulm.reset_index(drop=True, inplace=True)
    # Demencia
    cats = ['Demencia Padres', 'Demencia otros', 'Demencia Sin Antecedentes']
    data[['AntDemencia']] = data[['AntDemencia']].astype('category')
    dem = pd.DataFrame(enc.fit_transform(data[['AntDemencia']]).toarray())
    dem.columns = enc.categories_
    for name in cats:
        if name not in dem:
            dem[name] = 0
    dem = dem[['Demencia Padres', 'Demencia otros', 'Demencia Sin Antecedentes']]
    dem.reset_index(drop=True, inplace=True)
    # Tabaco
    cats = ['Ex/Fumador', 'No Fumador']
    data[['Tabaco']] = data[['Tabaco']].astype('category')
    tab = pd.DataFrame(enc.fit_transform(data[['Tabaco']]).toarray())
    tab.columns = enc.categories_
    for name in cats:
        if name not in tab:
            tab[name] = 0
    tab = tab[['Ex/Fumador', 'No Fumador']]
    tab.reset_index(drop=True, inplace=True)
    # Alcohol
    cats = ['Alcohol Elevado', 'Alcohol Moderado','Alcohol leve', 'Alcohol nunca']
    data[['Alcohol']] = data[['Alcohol']].astype('category')
    alc = pd.DataFrame(enc.fit_transform(data[['Alcohol']]).toarray())
    alc.columns = enc.categories_
    for name in cats:
        if name not in alc:
            alc[name] = 0
    alc = alc[['Alcohol Elevado', 'Alcohol Moderado','Alcohol leve', 'Alcohol nunca']]
    alc.reset_index(drop=True, inplace=True)

    # Drop column to add it at the end
    affected = data[['TNeurocog']]
    affected.reset_index(drop=True, inplace=True)

    # Drop original categorical columns
    data = data.drop(['Sexo', 'EstCivil', 'ActLaboral','FormatConvivencia',
                                'NivelEducativo', 'ActIntelectual', 'RelSociales','AntPsiquia',
                                'AntCardiologi','AntNeurolog','AntRenal','AntPulmonar',
                                'AntDemencia','Tabaco','Alcohol','TNeurocog'], axis=1)
    data.reset_index(drop=True, inplace=True)

    data = pd.concat([data, sexo, civil, laboral,
                                  convi, educ, intec,
                                  rels, psiq, card, neur, ren,
                                  pulm, dem, tab, alc,
                                  affected],axis=1)
    return data

# Dictionary to specify fields of synthetic data for Alzheimer-Balea database
alz_fields = {
    'Edad' : {
        'type' : 'numerical',
        'subtype' : 'integer'
    },
    'Sexo' : {
        'type' : 'categorical'
    },
    'EstCivil' : {
        'type' : 'categorical'
    },
    'ActLaboral' : {
        'type' : 'categorical'
    },
    'FormatConvivencia' : {
        'type' : 'categorical'
    },
    'NivelEducativo' : {
        'type' : 'categorical'
    },
    'ActIntelectual' : {
        'type' : 'categorical'
    },
    'RelSociales' : {
        'type' : 'categorical'
    },
    'AntPsiquia' : {
        'type' : 'categorical'
    },
    'AntCardiologi' : {
        'type' : 'categorical'
    },
    'AntNeurolog' : {
        'type' : 'categorical'
    },
    'AntRenal' : {
        'type' : 'categorical'
    },
    'AntPulmonar' : {
        'type' : 'categorical'
    },
    'AntDemencia' : {
        'type' : 'categorical'
    },
    'Tabaco' : {
        'type' : 'categorical'
    },
    'Alcohol' : {
        'type' : 'categorical'
    },
    'HTAanterior' : {
        'type' : 'numerical',
        'subtype' : 'integer'
    },
    'TAS' : {
        'type' : 'numerical',
        'subtype' : 'integer'
    },
    'TAD' : {
        'type' : 'numerical',
        'subtype' : 'integer'
    },
    'DM' : {
        'type' : 'numerical',
        'subtype' : 'integer'
    },
    'Barthel' : {
        'type' : 'numerical',
        'subtype' : 'integer'
    },
    'Hb' : {
        'type' : 'numerical',
        'subtype' : 'float'
    },
    'VCM' : {
        'type' : 'numerical',
        'subtype' : 'float'
    },
    'HCM' : {
        'type' : 'numerical',
        'subtype' : 'float'
    },
    'Plaquetas' : {
        'type' : 'numerical',
        'subtype' : 'float'
    },
    'Leucocitos' : {
        'type' : 'numerical',
        'subtype' : 'float'
    },
    'Neutrófilos' : {
        'type' : 'numerical',
        'subtype' : 'float'
    },
    'Linfocitos' : {
        'type' : 'numerical',
        'subtype' : 'float'
    },
    'Monocitos' : {
        'type' : 'numerical',
        'subtype' : 'float'
    },
    'Glucosa' : {
        'type' : 'numerical',
        'subtype' : 'float'
    },
    'Creatinina' : {
        'type' : 'numerical',
        'subtype' : 'float'
    },
    'FG' : {
        'type' : 'numerical',
        'subtype' : 'float'
    },
    'Na' : {
        'type' : 'numerical',
        'subtype' : 'float'
    },
    'K' : {
        'type' : 'numerical',
        'subtype' : 'float'
    },
    'ALT' : {
        'type' : 'numerical',
        'subtype' : 'float'
    },
    'LDL' : {
        'type' : 'numerical',
        'subtype' : 'float'
    },
    'TNeurocog' : {
        'type' : 'numerical',
        'subtype' : 'integer'
    },
 }

# Custom variable constraints to generate synthetic data
constraints = [
                #Positive('Edad',handling_strategy='reject_sampling'),
                #Binary('HTAanterior',handling_strategy='reject_sampling'),
                #Positive('TAS',handling_strategy='reject_sampling'),
                #Positive('TAD',handling_strategy='reject_sampling'),
                #Binary('DM',handling_strategy='reject_sampling'),
                # Positive('Barthel',handling_strategy='reject_sampling'),
                # Positive('Hb',handling_strategy='reject_sampling'),
                # Positive('VCM',handling_strategy='reject_sampling'),
                # Positive('HCM',handling_strategy='reject_sampling'),
                # Positive('Plaquetas',handling_strategy='reject_sampling'),
                # Positive('Leucocitos',handling_strategy='reject_sampling'),
                # Positive('Neutrófilos',handling_strategy='reject_sampling'),
                # Positive('Linfocitos',handling_strategy='reject_sampling'),
                # Positive('Monocitos',handling_strategy='reject_sampling'),
                # Positive('Glucosa',handling_strategy='reject_sampling'),
                # Positive('Creatinina',handling_strategy='reject_sampling'),
                # Positive('FG',handling_strategy='reject_sampling'),
                # Positive('Na',handling_strategy='reject_sampling'),
                # Positive('K',handling_strategy='reject_sampling'),
                # Positive('ALT',handling_strategy='reject_sampling'),
                # Positive('Colesterol',handling_strategy='reject_sampling'),
                # Positive('LDL',handling_strategy='reject_sampling'),
                #Binary('TNeurocog',handling_strategy='reject_sampling'),
                ]

# Distributions for each field (all set to univariate)
alz_distributions = {
    'Grupo' : 'univariate',
    'Edad' : 'univariate',
    'EstCivil' : 'univariate',
    'Actlaboral' : 'univariate',
    'FormatConvivencia' : 'univariate',
    'NivelEducativo' : 'univariate',
    'ActIntelectual' : 'univariate',
    'RelSociales' : 'univariate',
    'AntCardiologi' : 'univariate',
    'AntNeurolog' : 'univariate',
    'AntRenal' : 'univariate',
    'AntPulmonar' : 'univariate',
    'AntDemencia' : 'univariate',
    'Tabaco' : 'univariate',
    'Alcohol' : 'univariate',
    'HTAanterior' : 'univariate',
    'TAS' : 'univariate',
    'TAD' : 'univariate',
    'DM' : 'univariate',
    'Barthel' : 'univariate',
    'Hb' : 'univariate',
    'VCM' : 'univariate',
    'HCM' : 'univariate',
    'Plaquetas' : 'univariate',
    'Leucocitos' : 'univariate',
    'Neutrófilos' : 'univariate',
    'Linfocitos' : 'univariate',
    'Monocitos' : 'univariate',
    'Glucosa' : 'univariate',
    'Creatinina' : 'univariate',
    'FG' : 'univariate',
    'Na' : 'univariate',
    'K' : 'univariate',
    'ALT' : 'univariate',
    'Colesterol' : 'univariate',
    'LDL' : 'univariate',
    'TNeurocog' : 'univariate',
    }

################################################################################
#              CONSTANTS TO HANDLE/STORE/VISUALIZE OBTAINED RESULTS            #
################################################################################

# Path where directories are stored
DICT_PATH = r"C:\Users\aralmeida\OneDrive - Universidad de Las Palmas de Gran Canaria\Doctorado\codigo\synthetic_data_generation_framework\AlzheimerBALEA\results"

# Dataset name
dataset_name = 'ALZ-BALEA'

# Variables needed to handle dictionaries corresponding to the number of generated data samples
sizes_keys = ["quarter", "half", "unit", "double", "quadruple", "only-synth"]

# Balancing Methods
balance1 = "NC"
balance2 = "Borderline"

# Augmentation methods
augmen1 = "CTGAN"
augmen2 = "GC"

# Best and worst combinations
best_worst = ['Borderline + Sep. + GC', 'NC + CTGAN'] # might be wrong

# Best combination
best_method = 'Borderline + Sep. + GC' # might be wrong

# Name of the used Machine Learning models and associated colors
models = ['SVM','RF', 'XGB', 'KNN']
model_colors = ['b','r','k','g']

# Studied metrics
mets = ["PCD","MMD","KLD"]

# Strings containing combinations of SDG (Synthetic Data Generators)
comb1 = ("%s + %s") % (balance1, augmen1)
comb2 = ("%s + %s") % (balance1, augmen2)
comb3 = ("%s + %s") % (balance2, augmen1)
comb4 = ("%s + %s") % (balance2, augmen2)
comb5 = ("%s + Sep. + %s") % (balance1, augmen1)
comb6 = ("%s + Sep. + %s") % (balance1, augmen2)
comb7 = ("%s + Sep. + %s") % (balance2, augmen1)
comb8 = ("%s + Sep. + %s") % (balance2, augmen2)
comb9 = "%s" % (augmen1)
comb10 = "Sep. + %s" % (augmen1)

# Split CTGAN and Gaussian Copula methods to plot them separately
ctgan_combinations = [comb1, comb3, comb5, comb7]
gc_combinations = [comb2, comb4, comb6, comb8]

# Chosen colors for each synthetic data generation combinations
ctgan_colors = ["k","r","g","b"]
gc_colors = ["c","m","y","orange"]

################################################################################
#              CONSTANTS TO HANDLE/STORE/VISUALIZE OBTAINED RESULTS            #
################################################################################


import json
import shutil
from pathlib import Path

from fcc2zim.challenge import Challenge
from fcc2zim.constants import Global

def get_challenges_for_lang(tmp_path, language="english"):
    return Path(tmp_path, language).rglob("*.md")

def update_index(path: Path, superblock: str, slug: str, language="english"):
    index_path = path.joinpath("index.json")
    if not index_path.exists():
        index_path.write_bytes(json.dumps({}).encode("utf-8"))

    index = json.loads(index_path.read_text())
    if language not in index:
        index[language] = {}
    if superblock not in index[language]:
        index[language][superblock] = []
    if slug not in index[language][superblock]:
        index[language][superblock].append(slug)

    with open(index_path, "w") as outfile:
        json.dump(index, outfile, indent=4)

"""
Simply copies over the locales to the client locales path
"""

def write_locales_to_path(source: Path, curriculumdir: Path, language="english"):
    shutil.copytree(source, curriculumdir / "locales" / language)

def write_course_to_path(
    challenge_list: list[Challenge],
    superblock: str,
    course_slug: str,
    curriculumdir: Path,
):
    """Writes the course to the chosen path.

    Each individual Markdown challenge is written along
    with the meta data for the course.

    Finally, we udpate the root index.json file with the course, which allows
    us to render a page listing all available courses
    """
    curriculumdir.mkdir(parents=True, exist_ok=True)
    meta = {"challenges": []}

    for challenge in challenge_list:
        challenge_dest_path = curriculumdir.joinpath(
            challenge.course_superblock, challenge.course_slug
        )
        challenge_dest_path.mkdir(parents=True, exist_ok=True)
        shutil.copy2(challenge.path, challenge_dest_path.joinpath(challenge.path.name))
        meta["challenges"].append(
            {"title": challenge.title(), "slug": challenge.path.stem}
        )

    meta_path = curriculumdir.joinpath(superblock, course_slug, "_meta.json")
    meta_path.parent.mkdir(parents=True, exist_ok=True)
    with open(meta_path, "w") as outfile:
        json.dump(meta, outfile, indent=4)

    # Create an index with a list of the courses
    update_index(curriculumdir, superblock, course_slug, challenge_list[0].language)

def prebuild_command(
    course_csv: str,
    fcc_lang: str,
    curriculum_raw: Path,
    curriculum_dist: Path,
):
    """Transform raw data in curriculum_raw directory into pre-built data in
    curriculum_dist directory

    E.g. if lang in english:
    - <curriculum_dist>/index.json
        => { 'english': {'superblock': ['basic-javascript'] } }
    - <curriculum_dist>/english/<superblock>/<course_slug>/_meta.json
        => { challenges: [{slug, title}] }
    - <curriculum_dist>/english/<superblock>/<course_slug>/{slug}.md
    """
    Global.logger.info("Scraper: prebuild phase starting")

    curriculum_dist.mkdir(parents=True, exist_ok=True)
    shutil.rmtree(curriculum_dist)

    challenges = curriculum_raw.joinpath(
        "freeCodeCamp-main", "curriculum", "challenges"
    )
    locales = curriculum_raw.joinpath(
        "freeCodeCamp-main", "client", "i18n", "locales", fcc_lang
    )

    # eg. ['basic-javascript', 'debugging']
    for course in course_csv.split(","):
        Global.logger.debug(f"Prebuilding {course}")
        meta = json.loads(challenges.joinpath("_meta", course, "meta.json").read_text())
        # Get the order that the challenges should be completed in for <course>
        ids = [
            item[0] if isinstance(item, list) else item["id"]
            for item in meta["challengeOrder"]
        ]
        superblock = meta["superBlock"]

        challenge_list: list[Challenge] = []
        for file in get_challenges_for_lang(challenges, fcc_lang):
            challenge = Challenge(file)
            if challenge.course_superblock != superblock:
                continue
            # ID is a UUID the Challenge, the only add it to the challenge list if it's
            # a part of the course.
            if challenge.identifier() in ids:
                challenge_list.append(challenge)

        write_course_to_path(
            sorted(challenge_list, key=lambda x: ids.index(x.identifier())),
            superblock,
            course,
            curriculum_dist.joinpath("curriculum", fcc_lang),
        )

    # Copy all the locales for this language
    write_locales_to_path(locales, curriculum_dist, fcc_lang)
    Global.logger.info(f"Prebuilt curriculum into {curriculum_dist}")
    Global.logger.info("Scraper: prebuild phase finished")


# multiAgents.py
# --------------
# Licensing Information:  You are free to use or extend these projects for
# educational purposes provided that (1) you do not distribute or publish
# solutions, (2) you retain this notice, and (3) you provide clear
# attribution to UC Berkeley, including a link to http://ai.berkeley.edu.
#
# Attribution Information: The Pacman AI projects were developed at UC Berkeley.
# The core projects and autograders were primarily created by John DeNero
# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu).
# Student side autograding was added by Brad Miller, Nick Hay, and
# Pieter Abbeel (pabbeel@cs.berkeley.edu).

from util import manhattanDistance
from game import Directions
import random, util

from game import Agent

class ReflexAgent(Agent):
    """
    A reflex agent chooses an action at each choice point by examining
    its alternatives via a state evaluation function.

    The code below is provided as a guide.  You are welcome to change
    it in any way you see fit, so long as you don't touch our method
    headers.
    """

    def getAction(self, gameState):
        """
        You do not need to change this method, but you're welcome to.

        getAction chooses among the best options according to the evaluation function.

        Just like in the previous project, getAction takes a GameState and returns
        some Directions.X for some X in the set {NORTH, SOUTH, WEST, EAST, STOP}
        """
        # Collect legal moves and successor states
        legalMoves = gameState.getLegalActions()

        # Choose one of the best actions
        scores = [self.evaluationFunction(gameState, action) for action in legalMoves]
        bestScore = max(scores)
        bestIndices = [index for index in range(len(scores)) if scores[index] == bestScore]
        chosenIndex = random.choice(bestIndices)  # Pick randomly among the best

        "Add more of your code here if you want to"

        return legalMoves[chosenIndex]

    def evaluationFunction(self, currentGameState, action):
        """
        Design a better evaluation function here.

        The evaluation function takes in the current and proposed successor
        GameStates (pacman.py) and returns a number, where higher numbers are better.

        The code below extracts some useful information from the state, like the
        remaining food (newFood) and Pacman position after moving (newPos).
        newScaredTimes holds the number of moves that each ghost will remain
        scared because of Pacman having eaten a power pellet.

        Print out these variables to see what you're getting, then combine them
        to create a masterful evaluation function.
        """
        # Useful information you can extract from a GameState (pacman.py)
        successorGameState = currentGameState.generatePacmanSuccessor(action)
        newPos = successorGameState.getPacmanPosition()
        newFood = successorGameState.getFood()
        newGhostStates = successorGameState.getGhostStates()
        newScaredTimes = [ghostState.scaredTimer for ghostState in newGhostStates]

        "*** YOUR CODE HERE ***"
        # return successorGameState.getScore()
        score = successorGameState.getScore()
        foods = newFood.asList()
        f_distance = list()
        for i in foods:
            f_distance.append(manhattanDistance(newPos, i))

        if action == "Stop":
            score -= 200

        for i in range(len(newGhostStates)):
            if (util.manhattanDistance(newGhostStates[i].getPosition(), newPos) < 2) or (
                    newGhostStates[i].getPosition() == newPos and (newScaredTimes[i] == 0)):
                score -= 200

        if len(f_distance) > 0:
            min_f_distance = min(f_distance)
            score = score + (1 / min_f_distance)

        return score

def scoreEvaluationFunction(currentGameState):
    """
    This default evaluation function just returns the score of the state.
    The score is the same one displayed in the Pacman GUI.

    This evaluation function is meant for use with adversarial search agents
    (not reflex agents).
    """
    return currentGameState.getScore()

class MultiAgentSearchAgent(Agent):
    """
    This class provides some common elements to all of your
    multi-agent searchers.  Any methods defined here will be available
    to the MinimaxPacmanAgent, AlphaBetaPacmanAgent & ExpectimaxPacmanAgent.

    You *do not* need to make any changes here, but you can if you want to
    add functionality to all your adversarial search agents.  Please do not
    remove anything, however.

    Note: this is an abstract class: one that should not be instantiated.  It's
    only partially specified, and designed to be extended.  Agent (game.py)
    is another abstract class.
    """

    def __init__(self, evalFn='scoreEvaluationFunction', depth='2'):
        self.index = 0  # Pacman is always agent index 0
        self.evaluationFunction = util.lookup(evalFn, globals())
        self.depth = int(depth)

class MinimaxAgent(MultiAgentSearchAgent):
    """
    Your minimax agent (question 2)
    """

    def getAction(self, gameState):
        """
        Returns the minimax action from the current gameState using self.depth
        and self.evaluationFunction.

        Here are some method calls that might be useful when implementing minimax.

        gameState.getLegalActions(agentIndex):
        Returns a list of legal actions for an agent
        agentIndex=0 means Pacman, ghosts are >= 1

        gameState.generateSuccessor(agentIndex, action):
        Returns the successor game state after an agent takes an action

        gameState.getNumAgents():
        Returns the total number of agents in the game

        gameState.isWin():
        Returns whether or not the game state is a winning state

        gameState.isLose():
        Returns whether or not the game state is a losing state
        """
        "*** YOUR CODE HERE ***"
        ghostsNumbers = gameState.getNumAgents()

        def max_value_f(gameState, depth):
            max_value = float('-inf')
            depth -= 1

            if depth == 0:
                return self.evaluationFunction(gameState)

            if gameState.isLose() or gameState.isWin():
                return self.evaluationFunction(gameState)

            for a in gameState.getLegalActions(0):
                max_value = max(max_value, min_value_f(gameState.generateSuccessor(0, a), depth, 1))

            return max_value

        def min_value_f(gameState, depth, agentIndex):
            min_value = float('inf')

            if gameState.isLose() or gameState.isWin():
                return self.evaluationFunction(gameState)

            for a in gameState.getLegalActions(agentIndex):
                if agentIndex < (ghostsNumbers - 1):
                    min_value = min(min_value,
                                    min_value_f(gameState.generateSuccessor(agentIndex, a), depth, agentIndex + 1))
                else:
                    min_value = min(min_value, max_value_f(gameState.generateSuccessor(agentIndex, a), depth))

            return min_value

        max_value = float('-inf')
        result = 0

        for a in gameState.getLegalActions(0):
            action_value = min_value_f(gameState.generateSuccessor(0, a), self.depth, 1)

            if action_value > max_value:
                max_value = action_value
                result = a

        return result

class AlphaBetaAgent(MultiAgentSearchAgent):
    """
    Your minimax agent with alpha-beta pruning (question 3)
    """

    def getAction(self, gameState):
        """
        Returns the minimax action using self.depth and self.evaluationFunction
        """
        "*** YOUR CODE HERE ***"
        ghostsNumbers = gameState.getNumAgents()

        def max_value_f(gameState, depth, Alpha, Beta):
            depth = depth - 1
            max_value = float('-inf')

            if depth == 0:
                return self.evaluationFunction(gameState)

            if gameState.isLose() or gameState.isWin():
                return self.evaluationFunction(gameState)

            for a in gameState.getLegalActions(0):
                max_value = max(max_value, min_value_f(gameState.generateSuccessor(0, a), depth, 1, Alpha, Beta))
                if max_value > Beta:
                    return max_value

                Alpha = max(Alpha, max_value)

            return max_value

        def min_value_f(gameState, depth, agentIndex, Alpha, Beta):
            min_value = float('inf')  # minValue

            if gameState.isLose() or gameState.isWin():
                return self.evaluationFunction(gameState)

            for a in gameState.getLegalActions(agentIndex):
                if agentIndex < (ghostsNumbers - 1):
                    min_value = min(min_value, min_value_f(gameState.generateSuccessor(agentIndex, a), depth,
                                                           agentIndex + 1, Alpha, Beta))
                else:
                    min_value = min(min_value,
                                    max_value_f(gameState.generateSuccessor(agentIndex, a), depth, Alpha, Beta))
                if min_value < Alpha:
                    return min_value

                Beta = min(Beta, min_value)

            return min_value

        # main part
        result = 0
        Alpha = float('-inf')
        Beta = float('inf')

        for a in gameState.getLegalActions(0):
            action_value = min_value_f(gameState.generateSuccessor(0, a), self.depth, 1, Alpha, Beta)
            if action_value > Alpha:
                result = a
                Alpha = action_value

        return result

class ExpectimaxAgent(MultiAgentSearchAgent):
    """
      Your expectimax agent (question 4)
    """

    def getAction(self, gameState):
        """
        Returns the expectimax action using self.depth and self.evaluationFunction

        All ghosts should be modeled as choosing uniformly at random from their
        legal moves.
        """
        "*** YOUR CODE HERE ***"
        def expect_max(gameState, depth, agentcounter):
            if agentcounter >= gameState.getNumAgents():
                agentcounter = 0
                depth += 1

            if gameState.isLose() or gameState.isWin() or depth == self.depth:
                return self.evaluationFunction(gameState)

            elif agentcounter == 0:
                return max_value_f(gameState, depth, agentcounter)

            else:
                return get_expected_value(gameState, depth, agentcounter)

        def max_value_f(gameState, depth, agentcounter):
            max_value = ["", -float("inf")]

            if not gameState.getLegalActions(agentcounter):
                return self.evaluationFunction(gameState)

            for a in gameState.getLegalActions(agentcounter):
                curr = expect_max(gameState.generateSuccessor(agentcounter, a), depth, agentcounter + 1)
                if type(curr) is not list:
                    next_value = curr
                else:
                    next_value = curr[1]

                if next_value > max_value[1]:
                    max_value = [a, next_value]

            return max_value

        def get_expected_value(gameState, depth, agentcounter):
            max_value = ["", 0]

            if not gameState.getLegalActions(agentcounter):
                return self.evaluationFunction(gameState)

            for a in gameState.getLegalActions(agentcounter):
                curr_state = gameState.generateSuccessor(agentcounter, a)
                curr = expect_max(curr_state, depth, agentcounter + 1)
                if type(curr) is list:
                    next_value = curr[1]
                else:
                    next_value = curr

                max_value[0] = a
                max_value[1] += next_value * (1.0 / len(gameState.getLegalActions(agentcounter)))
            return max_value

        return expect_max(gameState, 0, 0)[0]

def betterEvaluationFunction(currentGameState):
    """
    Your extreme ghost-hunting, pellet-nabbing, food-gobbling, unstoppable
    evaluation function (question 5).

    DESCRIPTION: <write something here so we know what you did>
    """
    "*** YOUR CODE HERE ***"
    foods = []
    ghost_close = 0

    for food in currentGameState.getFood().asList():
        foods.append(manhattanDistance(currentGameState.getPacmanPosition(), food) * -1)
    if not foods:
        foods.append(0)

    if manhattanDistance(currentGameState.getGhostPositions()[0], currentGameState.getPacmanPosition()) == 0 and \
            currentGameState.getGhostStates()[0].scaredTimer == 0:
        ghost_close = -200

    elif currentGameState.getGhostStates()[0].scaredTimer > 0:
        ghost_close = -1 / (
            manhattanDistance(currentGameState.getGhostPositions()[0], currentGameState.getPacmanPosition()))

    return currentGameState.getScore() + ghost_close + len(currentGameState.getCapsules()) + max(foods)

# Abbreviation
better = betterEvaluationFunction


#!/usr/bin/python3

import lxml.etree

def test():
	tree = lxml.etree.parse('/home/msr/src/opengovernment/raw/usc18@113-21.xml')
	for section in tree.xpath('//a:section', namespaces={'a': 'http://xml.house.gov/schemas/uslm/1.0'}):
		traverse(section)

def traverse(node):
	for subnode in node.xpath('./a:content|./a:subsection', namespaces={'a': 'http://xml.house.gov/schemas/uslm/1.0'}):
		if subnode.tag == '{http://xml.house.gov/schemas/uslm/1.0}content':
			for p in subnode.iterfind('{http://xml.house.gov/schemas/uslm/1.0}p'):
				print(lxml.etree.tostring(p.text,encoding=str))
		elif subnode.tag == '{http://xml.house.gov/schemas/uslm/1.0}subsection':
			traverse(subnode)

def main():
	test()

if __name__ == '__main__':
	main()


#Utility functions for composite_wrapper
from .cmd_parser import composite_options
from .log import logger
from decimal import Decimal

def check_longitude_validity(min_longitude, max_longitude):
    lon_lower_bound = Decimal('-180')
    lon_upper_bound = Decimal('180')
    if min_longitude > max_longitude:
        raise Exception("minimum longitude must be less than maximum longitude")
    if min_longitude < lon_lower_bound or min_longitude > lon_upper_bound:
        raise Exception("minimum longitude is not a valid value for longitude")
    if max_longitude < lon_lower_bound or max_longitude > lon_upper_bound:
        raise Exception("maximum longitude is not a valid value for longitude")

def check_latitude_validity(min_latitude, max_latitude):
    lat_lower_bound = Decimal('-90')
    lat_upper_bound = Decimal('90')
    if min_latitude > max_latitude:
        raise Exception("minimum latitude must be less than maximum latitude")
    if min_latitude < lat_lower_bound or min_latitude > lat_upper_bound:
        raise Exception("minimum latitude is not a valid value for latitude")
    if max_latitude < lat_lower_bound or max_latitude > lat_upper_bound:
        raise Exception("maximum latitude is not a valid value for latitude")

def check_bounding_box_validity():
    lon_lat_min = composite_options.lon_lat_minimum
    lon_lat_max = composite_options.lon_lat_maximum
    check_longitude_validity(min_longitude=lon_lat_min[0], max_longitude=lon_lat_max[0])
    check_latitude_validity(min_latitude=lon_lat_min[1], max_latitude=lon_lat_max[1])
    logger.debug("Bounding box is valid")

def to_float(numerical_list):
    float_list = []
    for number in numerical_list:
        float_list.append(float(number))
    return float_list