# AUTOGENERATED! DO NOT EDIT! File to edit: notebooks/package/Field.ipynb (unless otherwise specified).

__all__ = ['parameters', 'DelegatedAttribute', 'delegate_as', 'Field', 'DictLine', 'Coding']

# Cell
import pandas as pd
import dask.dataframe as dd
import logging
import numpy as np
from ..data_catalog import get_catalog, get_parameters

# Cell
parameters= get_parameters()
parameters

# Cell
#export
try:
    data_dict = pd.read_csv(parameters["data_dict_file"])
    data_dict.file = parameters["data_dict_file"]
    pheno_df = dd.read_parquet(parameters["pheno_file"])
    if parameters["id_col"]:
        if parameters["id_col"] not in pheno_df.columns:
            raise KeyError(f"Cannot find ID column {parameters['id_col']} in file {parameters['id_col']}")
        logging.warning("Found ID column, setting index. This might take a bit long. Please be patient.")
        pheno_df[parameters["id_col"]] = pheno_df[parameters["id_col"]].astype(int)
        pheno_df = pheno_df.set_index(parameters["id_col"])
    coding_file_path_template = parameters["coding_file_path_template"]

except AttributeError:
    raise AttributeError("Could not find file names from the settings module, please set `data_dict_file` and `pheno_file` attribute")

# Cell
class DelegatedAttribute:
    def __init__(self, delegate_name, attr_name):
        self.attr_name = attr_name
        self.delegate_name = delegate_name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            print("attr_name",self.attr_name)
            print("delegate_name",  self.delegate_name)
            #print("instance",instance)
            print("owner", owner)

            # return instance.delegate.attr
            return getattr(self.delegate(instance),  self.attr_name)

    def __set__(self, instance, value):
        # instance.delegate.attr = value
        setattr(self.delegate(instance), self.attr_name, value)

    def __delete__(self, instance):
        delattr(self.delegate(instance), self.attr_name)

    def delegate(self, instance):
        return getattr(instance, self.delegate_name)

    def __str__(self):
        return ""

# def delegate_as(delegate_cls):
#     # gather the attributes of the delegate class to add to the decorated class
#     attributes = delegate_cls.__dict__.keys()

#     def inner(cls):
#         # create property for storing the delegate
#         setattr(cls, 'delegate', delegate_cls)
#         # set all the attributes
#         for attr in attributes:
#             setattr(cls, attr, DelegatedAttribute(to, attr))
#         return cls

#     return inner

def delegate_as(delegate_cls, to='delegate'):#, include=frozenset(), ignore=frozenset()):
    # turn include and ignore into sets, if they aren't already
#     if not isinstance(include, set):
#         include = set(include)
#     if not isinstance(ignore, set):
#         ignore = set(ignore)
    print(delegate_cls)
    delegate_attrs = set(delegate_cls.__dict__.keys())
    attributes = delegate_attrs#include | delegate_attrs - ignore

    def inner(cls):
        # create property for storing the delegate
        #setattr(cls, to, delegate_cls)

        # don't bother adding attributes that the class already has
        attrs = attributes - set(cls.__dict__.keys())
        print("attributes", attrs)
        print("dict_line" in attrs)
        print(cls)
        # set all the attributes
        for attr in attrs:
            setattr(cls, attr, DelegatedAttribute(to, attr))
        return cls
    return inner

# Cell

#@delegate_as(dd.core.DataFrame, to="df")
#@delegate_df_cls(dd.core.DataFrame)


#@add_numerics(dd.core.DataFrame, dunder_delegate_attr="col", other_delegate_attr="df", dunder_list = ['add', 'sub', 'mul', 'truediv', 'floordiv', 'mod', 'pow', 'radd', 'rsub', 'rmul', 'rtruediv', 'rfloordiv', 'rmod', 'rpow', 'lt', 'gt', 'le', 'ge', 'ne', 'eq'])
class Field():
#     DELEGATED_METHODS = {
#         'df': ['add','radd','sub','rsub','mul','rmul']
#     }
#     data_dict= data_dict
#     pheno_df = pheno_df

    def __init__(self,pheno, *, name="Unamed_Field", instances=None, arrays=None):
        #self.dd_cls = dd_cls
        #allow passing in both the FieldID int and Field str

        if isinstance(pheno, int):
            dict_line_df = data_dict.query(f"FieldID == '{pheno}'")
        else:
            dict_line_df = data_dict.query(f"Field == '{pheno}'")

        if dict_line_df.empty:
            raise ValueError(f"Cannot find Field/FieldID '{pheno}' in data dict file '{data_dict.file}' ")
        self.dict_line = DictLine(dict_line_df)


        #make dict_line attributes accessible from Field object
        for col in self.dict_line.df.columns:
            setattr(self, col, getattr(self.dict_line, f"_{col}"))

        #reminder of what attribute is available from self.dict_line
        self.help = self.dict_line.df.columns.tolist()
        self.pheno_str = self.Field
        self.pheno_cols = self.dict_line.get_pheno_cols(pheno_df.columns, instances=instances, arrays=arrays)

        try:
            self.df = pheno_df.loc[:, self.pheno_cols].fillna(np.nan)
        except KeyError:
            raise KeyError(f"Cannot find phenotype '{self.pheno_str}'', ID: {self.FieldID} in the phenotype file. Please make sure your phenotype file contains all following columns: {self.pheno_cols} ")

        self.name = name
        #some fields don't have coding
        if self.Coding:
            #replacing coding int with more versatile coding object
            self.Coding = self.get_coding(coding_file_path_template)
            self.get_codes = self.Coding.get_codes
        else:
            print(f"Data field {self} has no Coding. The Coding attribute will be 'None'")



    def copy(self):
        obj = type(self).__new__(self.__class__)
        obj.__dict__.update(self.__dict__)
        obj.df = self.df.copy()
        return obj

    @classmethod
    def init_multi_type(cls, data, name):
        accepted_types = (str, int)
        #create new instance(s) of class Field

        ##handling dict
        if isinstance(data, dict):
            field = data["pheno"]
            if isinstance(field, accepted_types):
                updated_kwargs = data
                updated_kwargs["name"] = name
                return cls(**data)
        elif isinstance(data, accepted_types):
            return cls(data, name=name)

        #create copy of instance with a different name
        elif isinstance(data, cls):
            new_field = data.copy()
            new_field.name = name
            return new_field
        else:
            raise TypeError(f"Invalid pheno data type {type(field)}, can only accept input of type {accepted_types} ")

    @classmethod
    def make_fields_dict(cls, data: dict)-> dict:
        if isinstance(data, dict):
            iter_obj = data.items()
            return {name: cls.init_multi_type(dict_or_obj, name) for name, dict_or_obj in iter_obj}
        raise TypeError("Can only accept dictionary ")

    @property
    def name(self, ):
        return self._name

    @name.setter
    def name(self,new_name):
        def make_full_name(orig_name, new_name=None):
            if new_name is None:
                new_name = "Unamed_field"
            #for first time parsing from source
            if "." in orig_name:
                name_list = orig_name.split(".")
                instance = name_list[2]
                array = name_list[3]
            else:
                name_list = orig_name.split("_")
                instance = name_list[-2]
                array = name_list[-1]
            return f"{new_name}_{instance}_{array}"

        #make_name = partial(make_full_name, new_name=new_name)

        #if only one instance and array, then we simplify the column name
        if self.Array == self.Instances == 1:
            self.df.columns = [new_name for col in self.df.columns]
        else:
            self.df.columns = [ make_full_name(col, new_name=new_name) for col in self.df.columns]

        self._name = new_name
        return self

    def rename(self, new_name):
        self.name = new_name
        return self

    @property
    def pheno_str_no_space(self):
        return self.pheno_str.replace(" ", "_")

    def __repr__(self):
        return f"Field(Name:{self.name}, Pheno: `{self.pheno_str}`, ID: {self.FieldID}, Original Column(s): {self.pheno_cols}, Named Column(s): {self.all_cols_df.columns})"

    def __str__(self):
        return self.__repr__()

    def get_coding(self, coding_file_path_template):
        coding_file_name = coding_file_path_template.replace("*", str(self.Coding))
        return(Coding(coding_file_name, self.Coding))


    #need to call .compute() before you can perform masking
    def get_attr_childs(self, attr,*, input_field, output_field):
        all_related_fields = self.Coding.get_codes(attr, input_field, output_field)
        print(f"All the related fields that are classified as '{attr}' in coding file \n '{self.Coding.coding_file}' \n are {all_related_fields}")
        return all_related_fields #(self.df[self.pheno_str_no_space]).isin(all_related_fields)

    @property
    def value_counts(self):
        return self.col.value_counts()

    @property
    def all_cols_df(self):
        col = self.df.loc[:,self.df.columns]
        if (self.ValueType).lower() == "continuous":
            return col.astype("float")
        return col


class DictLine():
    def __init__(self, dict_line_df):
        self.df = dict_line_df

        #turn columns into private attributes _{col}
        for col in self.df.columns:
            setattr(self.__class__, f"_{col}", self.get_dict_field(col, self.df))

    def get_dict_field(self,dict_field, dict_line):
        @property
        def func(self):
            #catch multiple rows dataframes
            try:
                field = self.df[dict_field].item()
            except ValueError:
                return None

            if dict_field == "Coding":
                coding_num = field
                #some fields do not have coding
                if np.isnan(coding_num):
                    return None
                else:
                    return int(coding_num)
            return field
        return func

    def get_pheno_cols(self, col_list, instances=None, arrays=None):

        all_cols=[col for col in col_list if col.startswith(f"f.{self._FieldID}.")]

        if instances:
            all_cols = [col for col in all_cols if (int(col.split(".")[2]) in instances)]

        if arrays:
            all_cols = [col for col in all_cols if (int(col.split(".")[3]) in arrays)]

        return all_cols #[f"f.{self._FieldID}.{instance}.{array}" for array in range(self._Array) for instance in range(self._Instances)]

class Coding():
    def __init__(self, coding_file, coding_num):
        self.coding_num = coding_num
        self.coding_file = coding_file
        self.df = pd.read_csv(coding_file, sep="\t")

    def __repr__(self):
        return f"{self.__class__}. Coding num: {self.coding_num}, coding file: {self.coding_file}"

    def get_codes(self, selection, input_field, output_field):
        valid_coding_fields = ["coding","meaning","node_id"]
        for i_o_field in [input_field,output_field]:
            if (i_o_field not in valid_coding_fields):
                raise ValueError(f"Could not find field {i_o_field} field in coding file {self.coding_file}, choose from the following options: {valid_coding_fields}")
        if selection not in self.df[input_field].values:
            raise ValueError(f"Value not found in field {input_field} of coding file")

        code_line = DictLine(self.df[self.df[input_field] == selection])

        codes = [getattr(code_line,f"_{output_field}")]

        #code_line = DictLine(coding_df.query(f"meaning == '{selection}'"))
        df = self.df.query(f"parent_id=={code_line._node_id}")
        child_nodes = df[input_field].values.tolist()

        #recurse on the child nodes to get codings of level below the selection
        for child in child_nodes:
            codes.extend(self.get_codes(child, input_field, output_field))
        return codes