# cython: language_level=3

from enum import Enum


class _PropType(Enum):
    Bool = 0
    Integer = 1
    Enumerated = 2
    Real = 3


class _NullConverter:
    def from_py(self, val):
        return val

    def to_py(self, val):
        return val


class _EnumConverter:
    def __init__(self, enum_type):
        self.enum_type = enum_type

    def from_py(self, val):
        try:
            return val.value
        except AttributeError:
            pass

        try:
            return self.enum_type[val].value
        except KeyError:
            raise AttributeError('Invalid attribute value \'{0}\''.format(val))

    def to_py(self, val):
        return self.enum_type(val)


class _Prop:
    def __init__(self, value, prop_type, converter):
        self.value = value
        self.prop_type = prop_type
        self.converter = converter

    def to_py(self, val):
        return self.converter.to_py(val)

    def from_py(self, val):
        return self.converter.from_py(val)

    @staticmethod
    def boolean(value):
        return _Prop(value, _PropType.Bool, _NullConverter())

    @staticmethod
    def integer(value):
        return _Prop(value, _PropType.Integer, _NullConverter())

    @staticmethod
    def enumerated(value, enum_type):
        return _Prop(value, _PropType.Enumerated, _EnumConverter(enum_type))

    @staticmethod
    def real(value):
        return _Prop(value, _PropType.Real, _NullConverter())


cdef dict opt_prop_map = {
    # Boolean properties
    'perform_newton_step':   _Prop.boolean(csleqp.SLEQP_SETTINGS_BOOL_PERFORM_NEWTON_STEP),
    'global_penalty_resets': _Prop.boolean(csleqp.SLEQP_SETTINGS_BOOL_GLOBAL_PENALTY_RESETS),
    'perform_soc':           _Prop.boolean(csleqp.SLEQP_SETTINGS_BOOL_PERFORM_SOC),
    'use_quadratic_model':   _Prop.boolean(csleqp.SLEQP_SETTINGS_BOOL_USE_QUADRATIC_MODEL),
    'enable_restoration':    _Prop.boolean(csleqp.SLEQP_SETTINGS_BOOL_ENABLE_RESTORATION_PHASE),
    'enable_preprocessor':   _Prop.boolean(csleqp.SLEQP_SETTINGS_BOOL_ENABLE_PREPROCESSOR),
    'lp_resolves':           _Prop.boolean(csleqp.SLEQP_SETTINGS_BOOL_LP_RESOLVES),

    # Integer properties
    'num_quasi_newton_iterates': _Prop.integer(csleqp.SLEQP_SETTINGS_INT_NUM_QUASI_NEWTON_ITERATES),
    'max_newton_iterations':     _Prop.integer(csleqp.SLEQP_SETTINGS_INT_MAX_NEWTON_ITERATIONS),
    'num_threads':               _Prop.integer(csleqp.SLEQP_SETTINGS_INT_NUM_THREADS),

    # SLEQP_SETTINGS_INT_FLOAT_WARNING_FLAGS,
    # SLEQP_SETTINGS_INT_FLOAT_ERROR_FLAGS,

    # Enumerated properties
    'deriv_check':          _Prop.enumerated(csleqp.SLEQP_SETTINGS_ENUM_DERIV_CHECK, DerivCheck),
    'hessian_eval':         _Prop.enumerated(csleqp.SLEQP_SETTINGS_ENUM_HESS_EVAL, HessianEval),
    'dual_estimation_type': _Prop.enumerated(csleqp.SLEQP_SETTINGS_ENUM_DUAL_ESTIMATION_TYPE, DualEstimationType),
    'initial_tr_choice':    _Prop.enumerated(csleqp.SLEQP_SETTINGS_ENUM_INITIAL_TR_CHOICE, InitialTRChoice),
    'bfgs_sizing':          _Prop.enumerated(csleqp.SLEQP_SETTINGS_ENUM_BFGS_SIZING, Sizing),
    'tr_solver':            _Prop.enumerated(csleqp.SLEQP_SETTINGS_ENUM_TR_SOLVER, TRSolver),
    'polishing_type':       _Prop.enumerated(csleqp.SLEQP_SETTINGS_ENUM_POLISHING_TYPE, PolishingType),
    'step_rule':            _Prop.enumerated(csleqp.SLEQP_SETTINGS_ENUM_STEP_RULE, StepRule),
    'linesearch':           _Prop.enumerated(csleqp.SLEQP_SETTINGS_ENUM_LINESEARCH, LineSearch),
    'parametric_cauchy':    _Prop.enumerated(csleqp.SLEQP_SETTINGS_ENUM_PARAMETRIC_CAUCHY, ParametricCauchy),
    'aug_jac_method':       _Prop.enumerated(csleqp.SLEQP_SETTINGS_ENUM_AUG_JAC_METHOD, AugJacMethod),

    'zero_eps':           _Prop.real(csleqp.SLEQP_SETTINGS_REAL_ZERO_EPS),
    'eps':                _Prop.real(csleqp.SLEQP_SETTINGS_REAL_EPS),
    'deriv_perturbation': _Prop.real(csleqp.SLEQP_SETTINGS_REAL_DERIV_PERTURBATION),
    'deriv_tol':          _Prop.real(csleqp.SLEQP_SETTINGS_REAL_DERIV_TOL),
    'cauchy_tau':         _Prop.real(csleqp.SLEQP_SETTINGS_REAL_CAUCHY_TAU),
    'cauchy_eta':         _Prop.real(csleqp.SLEQP_SETTINGS_REAL_CAUCHY_ETA),
    'linesearch_tau':     _Prop.real(csleqp.SLEQP_SETTINGS_REAL_LINESEARCH_TAU),
    'linesearch_eta':     _Prop.real(csleqp.SLEQP_SETTINGS_REAL_LINESEARCH_ETA),
    'linesearch_cutoff':  _Prop.real(csleqp.SLEQP_SETTINGS_REAL_LINESEARCH_CUTOFF),
    'feas_tol':           _Prop.real(csleqp.SLEQP_SETTINGS_REAL_FEAS_TOL),
    'slack_tol':          _Prop.real(csleqp.SLEQP_SETTINGS_REAL_SLACK_TOL),
    'stat_tol':           _Prop.real(csleqp.SLEQP_SETTINGS_REAL_STAT_TOL),
    'accepted_reduction': _Prop.real(csleqp.SLEQP_SETTINGS_REAL_ACCEPTED_REDUCTION),
    'deadpoint_bound':    _Prop.real(csleqp.SLEQP_SETTINGS_REAL_DEADPOINT_BOUND)
}

cdef class Settings:
    cdef csleqp.SleqpSettings * settings

    def __cinit__(self):
        csleqp_call(csleqp.sleqp_settings_create( & self.settings))

    def __init__(self, **values):
        for key, value in values.items():
            setattr(self, key, value)

    def props(self) -> dict:
        return {key: getattr(self, key) for key in opt_prop_map}

    def __str__(self) -> str:
        return 'Settings: {0}'.format(self.props())

    def __repr__(self) -> str:
        return 'Settings({0})'.format(repr(self.props()))

    cpdef read(self, str filename):
        csleqp_call(csleqp.sleqp_settings_read_file(self.settings,
                                                    filename))

    cdef _prop(self, name):
        prop = opt_prop_map.get(name)
        if prop is None:
            raise AttributeError(
                'Settings has no attribute \'{0}\''.format(name))
        return prop

    def __getattr__(self, name):
        prop = self._prop(name)
        prop_val = None

        if prop.prop_type == _PropType.Integer:
            prop_val = csleqp.sleqp_settings_int_value(self.settings,
                                                       prop.value)
        elif prop.prop_type == _PropType.Real:
            prop_val = csleqp.sleqp_settings_real_value(self.settings,
                                                        prop.value)
        elif prop.prop_type == _PropType.Enumerated:
            prop_val = csleqp.sleqp_settings_enum_value(self.settings,
                                                        prop.value)
        else:
            assert prop.prop_type == _PropType.Bool

            prop_val = csleqp.sleqp_settings_bool_value(self.settings,
                                                        prop.value)

        return prop.to_py(prop_val)

    def __setattr__(self, name, value):
        prop = self._prop(name)
        prop_val = prop.from_py(value)

        if prop.prop_type == _PropType.Integer:
            csleqp_call(csleqp.sleqp_settings_set_int_value(self.settings,
                                                            prop.value,
                                                            prop_val))
        elif prop.prop_type == _PropType.Real:
            csleqp_call(csleqp.sleqp_settings_set_real_value(self.settings,
                                                             prop.value,
                                                             prop_val))
        elif prop.prop_type == _PropType.Enumerated:
            csleqp_call(csleqp.sleqp_settings_set_enum_value(self.settings,
                                                             prop.value,
                                                             prop_val))
        else:
            assert prop.prop_type == _PropType.Bool

            csleqp_call(csleqp.sleqp_settings_set_bool_value(self.settings,
                                                             prop.value,
                                                             prop_val))
