from __future__ import annotations

import inspect
from typing import Sized

from numpy import inf

from ._meta import MultiModel
from .parameter import Parameters, Parameter


class Model:
    def __init__(self, model, *args, **kwargs):
        self.idx = kwargs.pop('idx', None)

        self.is_multi = False
        if inspect.isclass(model) or isinstance(model, MultiModel):
            self._init_from_class(model)
        elif inspect.isfunction(model):
            self._init_from_function(model)
        else:
            raise ValueError(f'No idea how to use datatype {model!r}.')

        self.lb = [i if i is not None else -inf for i in self.lb]
        self.ub = [i if i is not None else inf for i in self.ub]

        self.parameter = Parameters()
        self.is_complex = None
        self._complex_part = False

        if 'complex' in kwargs:
            self.set_complex(kwargs.pop('complex'))

        if args:
            self.fun_args = args
        else:
            self.fun_args = []

        self.fun_kwargs.update(kwargs)

    def _init_from_function(self, func):
        self.name = str(func)
        self._int_func = func
        self._int_iter = func

        self.params = []
        self.fun_kwargs = {}
        for k, v in inspect.signature(func).parameters.items():
            if v.default != inspect.Parameter.empty:
                self.fun_kwargs[k] = v.default
            else:
                self.params.append(k)
        # first parameter is x
        self.params.pop(0)

        self.lb = [None] * len(self.params)
        self.ub = [None] * len(self.params)

    def _init_from_class(self, model):
        self.name = model.name
        self.params = model.params
        self._int_func = model.func
        if hasattr(model, 'subs'):
            self._int_iter = model.subs
            self._iter_name = model.sub_name
            self.is_multi = True
        else:
            self._int_iter = model.func

        try:
            self.lb, self.ub = list(zip(*model.bounds))
        except AttributeError:
            self.lb = [None] * len(self.params)
            self.ub = [None] * len(self.params)

        if isinstance(model, MultiModel):
            self.fun_kwargs = model.fun_kwargs
        else:
            self.fun_kwargs = {k: v.default for k, v in inspect.signature(model.func).parameters.items()
                               if v.default is not inspect.Parameter.empty}

    def set_global_parameter(self,
                             key: str | Parameter,
                             value: float | str = None,
                             *,
                             var: bool = None,
                             lb: float = None,
                             ub: float = None,
                             default_bounds: bool = False,
                             ) -> Parameter:

        if isinstance(key, Parameter):
            p = key
            key = f'p{next(Parameters.parameter_counter)}'
            self.parameter.add_parameter(key, p)

        else:
            idx = [self.params.index(key)]
            if default_bounds:
                if lb is None:
                    lb = [self.lb[i] for i in idx]
                if ub is None:
                    ub = [self.lb[i] for i in idx]

            p = self.parameter.add(key, value, var=var, lb=lb, ub=ub)
            p.is_global = True

        return p

    @staticmethod
    def _prep(param_len, val):
        if isinstance(val, Sized):
            if len(val) != param_len:
                raise ValueError('Length mismatch for global parameter')
        else:
            return [val] * param_len

    def __str__(self):
        msg = 'Model: ' + self.name
        return msg

    def __len__(self):
        return len(self.params)

    def func(self, p, x, **kwargs):
        if not kwargs:
            kwargs = self.fun_kwargs

        f = self._int_func(x, *p, *self.fun_args, **kwargs)

        return f

    def sub(self, p, x, **kwargs):
        if not kwargs:
            kwargs = self.fun_kwargs

        if not self.is_multi:
            return []
        else:
            return list(self._int_iter(x, *p, *self.fun_args, **kwargs))

    def sub_name(self):
        if not self.is_multi:
            return []
        else:
            return list(self._iter_name())
