import .log
import .context
import .settings

class Compute():
    def __init__(self, data, user_file=None, directory=None):
        if user_file is None:
            user_file = "_lynguine.yml"
        if directory is None:
            directory = "."
        self._user_file = user_file
        self._directory = directory
        
        self._settings = settings.Settings(user_file=self._user_file, directory=self._directory)
        self._cntxt = context.Context(name="lynguine")
           
        self._log = log.Logger(
            name=__name__,
            level=self._cntxt["logging"]["level"],
            filename=self._cntxt["logging"]["filename"],
            directory = self._directory,
            
        )

        self._list_functions = self._compute_functions_list()

        self._computes = {}
        for comptype in self.comptypes:
            self._computes[comptype]=[]
            if comptype in self._settings:
                if type(self._settings[comptype]) is list:
                    computes = self._settings[comptype]
                else:
                    computes = [self._settings[comptype]]

                for compute in computes:
                    self._computes[comptype].append(
                        self.prep(compute)
                    )

    def comptypes(self):
        return ["compute"]
    
    def prep(self, compute):
        """Prepare a compute entry for use."""
        compute_prep = {
            "function": self.gcf_(function=compute["function"]),
            "args" : self.gca_(**compute),
            "refresh" : "refresh" in compute and compute["refresh"],
        }
        if "field" in compute:
            compute_prep["field"] = compute["field"]
        return compute_prep
            
    def _compute_functions_list(self):
        """Return a list of compute functions."""
        pass
    
    def gca_(self, function, field=None, refresh=False, args={}, row_args={}, view_args={}, function_args={}, subseries_args={}, column_args={}):
        """Args generator for compute functions."""

        found_function = False
        for list_function in self._list_functions:
            if list_function["name"] == function:
                found_function = True
                break
        if not found_function:
            errmsg = f"Function \"{function}\" not found in list_functions."
            self._log.error(errmsg)
            raise ValueError(errmsg)
        return {
            "subseries_args" : subseries_args,
            "column_args" : column_args,
            "row_args" : row_args,
            "view_args" : view_args,
            "function_args" : function_args,
            "args" : args,
            "default_args" : list_function["default_args"],
        }


    def gcf_(self, function):
        """Function generator for compute functions."""
        found_function = False
        for list_function in self._list_functions:
            if list_function["name"] == function:
                found_function = True
                break
        if not found_function:
            raise ValueError(f"Function \"{function}\" not found in list_functions.")

        def compute_function(args={}, subseries_args={}, column_args={}, row_args={}, view_args={}, function_args = {}, default_args={}):
            """Compute a function using arguments found in subseries (column of sub-series specified by value in dictionary), or columns (full column specified by value in dictionary) or the same row (value from row as specified in the dictionary)."""

            kwargs = default_args.copy()
            kwargs.update(args)
            for key, value in function_args.items():
                kwargs[key] = self.gcf_(value)
            for key, column in column_args.items():
                if otherdf is None:
                    orig_col = self._data.get_column()
                    self._data.set_column(column)
                if key in kwargs:
                    self._log.warning(f"No key \"{key}\" already column_args found in kwargs.")
                kwargs[key] = self._data.get_column_values()
                self._data.set_column(orig_col)
                
            for key, column in subseries_args.items():
                orig_col = self._data.get_column()
                self._data.set_column(column)
                if key in kwargs:
                    self._log.warning(f"No key \"{key}\" from subseries_args already found in kwargs.")   
                kwargs[key] = self._data.get_subseries_values()
                self._data.set_column(orig_col)

            ## Arguments based on liquid, or format, or join.
            for key, view in view_args.items():
                orig_col = self._data.get_column()
                kwargs[key] = self._data.view_to_value(view)
                self._data.set_column(orig_col)
                
            for key, column in row_args.items():
                if key in kwargs:
                    self._log.warning(f"No key \"{key}\" from row_args already found in kwargs.")
                kwargs[key] = self._data.get_value_column(column)
            # kwargs.update(remove_nan(self._data.mapping(args)))
            self._log.debug(f"The keyword arguments for the compute function are {kwargs}.")
            return list_function["function"](**kwargs)

        compute_function.__name__ = list_function["name"]
        if "docstr" in list_function:
            compute_function.__docstr__ = list_function["docstr"]
        return compute_function

    def run(self, compute, df=None, index=None, refresh=True):
        """Run the computation given in compute."""
        multi_output = False
        fname = compute["function"].__name__
        fargs = compute["args"]
        if index is None:
            index = self._data.get_index()

        if "field" in compute:
            columns = compute["field"]
            if type(columns) is list:
                multi_output = True
            else:
                columns = [columns]
        else:
            columns = None

        # Determine which current values of field aren't set
        missing_vals = []
        if columns is not None:
            for column in columns:
                if df is None:
                    if column not in self._data.columns:
                        missing_vals.append(True)
                        continue
                else:
                    if column not in df.columns:
                        missing_vals.append(True)
                        continue
                if column == "_": # If the column is called "_" then ignore that argument
                    missing_vals.append(False)
                    continue
                if df is None:
                    val = self._data.get_value_column(column)
                else:
                    val = df.at[index, column]
                if type(val) is not list and pd.isna(val):
                    missing_vals.append(True)
                else:
                    missing_vals.append(False)

        if refresh or any(missing_vals) or columns is None:
            if columns is not None:
                self._log.debug(f"Running compute function \"{fname}\" storing in field(s) \"{columns}\" with index=\"{index}\" with refresh=\"{refresh}\" and arguments \"{fargs}\".")
            else:
                self._log.debug(f"Running compute function \"{fname}\" with no field(s) stored for index=\"{index}\" with refresh=\"{refresh}\" and arguments \"{fargs}\".")

            new_vals = compute["function"](**fargs)
        else:
            return

        if multi_output and type(new_vals) is not tuple:
            errmsg = f"Multiple columns provided for return values of \"{fname}\" but return value given is not a tuple."
            self._log.error(errmsg)
            raise ValueError(errmsg)
            
        if columns is None:
            return new_vals
        else:
            if multi_output:
                new_vals = [*new_vals]
            else:
                new_vals = [new_vals]
            for column, new_val, missing_val in zip(columns, new_vals, missing_vals):
                if column == "_":
                    continue
                if refresh or missing_val and self._data.writable:
                    self._log.debug(f"Setting column {column} in data structure to value {new_val} from compute.")
                    self._data.set_value_column(new_val, column)
  
    def run_all(self, df=None, index=None, pre=False, post=False):
        """Run any computation elements on the data frame."""

        self._log.debug(f"Running computes on index=\"{index}\" with pre=\"{pre}\" and post=\"{post}\"")
        computes = []
        if pre:
            computes += self._computes["precompute"]
        computes += self._computes["compute"]
        if post:
            computes += self._computes["postcompute"]
            
        for compute in computes:
            field = compute["field"]
            if self._data.writable(field):
                if compute["refresh"]:
                    self.run(compute, df, index, refresh=True)
                else:                    
                    self.run(compute, df, index, refresh=False)
            else:
                self._log.warning(f"Attempted to write to unwritable field \"{field}\"")
    
