"""
This module contains methods for generating _variables used in optimization, particular those related to APSIM replacement
"""
from dataclasses import dataclass
from typing import Union

import numpy as np

from apsimNGpy.core.apsim import ApsimModel

Var_types = ['int', 'float', 'choice_var', 'grid_var']


def _auto_data(data_type, bounds: tuple, step=None):
    if data_type not in ['int', 'float', int, float, np.int8, np.int32, np.int16, np.float32, np.float64]:
        raise ValueError('data-child not supported')
    if isinstance(data_type, str):
        data_type = eval(data_type)
    data = np.arange(bounds[0], bounds[1], step=step).astype(data_type)
    return data


class ContinuousVar:
    __slots__ = ['bounds', 'data_type', 'steps', ]

    def __init__(self, bounds: tuple, data_type: Union[int, float], steps: Union[int, float] = None):
        self.bounds = bounds
        self.data_type = data_type
        self.steps = steps

    @property
    def values(self):
        return _auto_data(self.data_type, self.bounds, self.steps)


class ChoiceVar:
    __slots__ = ['values']

    def __init__(self, categories: [int, str, float] = None):
        self.values = categories


@dataclass(slots=True, frozen=True)
class CropVar:
    updater: str
    main_param: str
    params: dict
    label: str
    var_desc: Union[ContinuousVar, ChoiceVar]


def _doc(section_desc):
    return f"""
    Any parameters that is in the `{section_desc}` of apsim model can be optimized by calling this function
    @param params:is a dictionary that could hold extra argument for each function.
    @param label: name suffix for the control variable being optimized
    @param var_desc: instance of `{ContinuousVar}` or `{ChoiceVar}` for categorical _variables. Big up to the authors of this 
    package we wrap around their variable description to facilitate mixed variable optimization. @param main_param: 
    is main_param arguments @param updater: method of ApsimModel class to update the parameters during the 
    optimization @return:  instance of `{CropVar}`
    """


def _evaluate_args(updater, main_param, params, label, var_desc):
    assertion_msg = 'params  must be a dict '
    if not isinstance(params, dict):
        raise ValueError(assertion_msg)
    if not isinstance(label, str):
        raise ValueError('label must be a string')
    if not isinstance(main_param, str):
        raise ValueError('main param must be defined as a string')
    if not isinstance(var_desc, (ContinuousVar, ChoiceVar)):
        raise ValueError(f"Var_desc must be any of: '{[ContinuousVar, ChoiceVar]}")
    try:
        # CHECK IF UPDATOR IS A valid attribute from ApsimModel
        getattr(ApsimModel, updater)
    except AttributeError as e:
        raise AttributeError(f'{updater} is not a valid method for updating parameters')


def manager(params: dict, label: str, var_desc, main_param=None, updater=None) -> CropVar:
    """
    Any parameters that is in the manager script of apsim model can be optimized by calling this function
    @param params:is a dictionary that could hold extra argument for each function.
    @param label: name suffix for the control variable being optimized
    @param var_desc: instance of wrapdisc.var big up to the authors of this package we wrap around their
     variable description to facilitate mixed variable optimization.
    @param main_param: is function updator  arguments which will hold the trial values during optimization
    @param updater: method of ApsimModel class to update the parameters during the optimization
    @return:  instance of CropVar
    """
    if updater is None:
        updater = 'update_mgt_by_path'
        main_param = 'param_values'

    _evaluate_args(updater, main_param, params, label, var_desc)

    return CropVar(updater, main_param, params, label, var_desc)


def soil(params: dict, label: str, var_desc, main_param=None, updater=None):
    if updater is None:
        updater = 'replace_soil_properties_by_path'  # example path 'Simulation.Soil.physical.None.None.BD'
        main_param = "param_values"
    _evaluate_args(updater, main_param, params, label, var_desc)
    return CropVar(updater, main_param, params, label, var_desc)


def cultivar(params: dict, label: str, var_desc, main_param=None, updater=None):
    if updater is None:
        updater = 'edit_cultivar'
        main_param = 'values'
    _evaluate_args(updater, main_param, params, label, var_desc)
    return CropVar(updater, main_param, params, label, var_desc)


def dates(params: dict, label: str, var_desc, main_param=None, updater=None):
    if updater is None:
        updater = 'change_simulation_dates'
        main_param = 'start_date'
    _evaluate_args(updater, main_param, params, label, var_desc)
    return CropVar(updater, main_param, params, label, var_desc)


soil.__doc__ = _doc('soil child')
manager.__doc__ = _doc('manager script')
cultivar.__doc__ = _doc('cultivar child')
dates.__doc__ = _doc('dates or clock child')
