# -*- coding: utf-8 -*-
"""
This module collects object representing rate expressions. It is based
on the ``chemp.util._expr`` module. The API is somewhat cumbersome since
it tries to be compatible with pure python, SymPy and the underlying
units library of ChemPy (``quantities``). Consider the API to be provisional.
"""


from collections import OrderedDict
from functools import reduce
import math
from operator import add

from ..units import get_derived_unit, default_units, energy, concentration
from ..util._dimensionality import dimension_codes, base_registry
from ..util.pyutil import memoize, deprecated
from ..util._expr import Expr, UnaryWrapper, Symbol


_molar = getattr(default_units, "molar", 1)  # makes module importable.


class RateExpr(Expr):
    """Baseclass for rate expressions, see source code of e.g. MassAction & Radiolytic."""

    @classmethod
    @deprecated(use_instead=Expr.from_callback)
    def subclass_from_callback(cls, cb, cls_attrs=None):
        """Override RateExpr.__call__

        Parameters
        ----------
        cb : callback
            With signature (variables, all_args, backend) -> scalar
            where `variables` is a dict, `all_args` a tuple and `backend` a module.
        cls_attrs : dict, optional
            Attributes to set in subclass, e.g. parameter_keys, nargs

        Examples
        --------
        >>> from chempy import Reaction
        >>> rxn = Reaction({'O2': 1, 'H2': 1}, {'H2O2': 1})  # d[H2O2]/dt = p0*exp(-p1/T)*sqrt([O2])
        >>> def cb(variables, all_args, backend):
        ...     O2, T = variables['O2'], variables['temperature']
        ...     p0, p1 = all_args
        ...     return p0*backend.sqrt(O2)*backend.exp(-p1/T)
        >>> MyRateExpr = RateExpr.subclass_from_callback(cb, dict(parameter_keys=('temperature',),nargs=2))
        >>> k = MyRateExpr([1.3e9, 4317.2])
        >>> print('%.5g' % k({'temperature': 298.15, 'O2': 1.1e-3, 'rxn': rxn}))
        22.186

        """

        class _RateExpr(cls):
            def __call__(self, variables, backend=math, **kwargs):
                return cb(
                    variables, self.all_args(variables), backend=backend, **kwargs
                )

        for k, v in (cls_attrs or {}).items():
            setattr(_RateExpr, k, v)
        return _RateExpr


class RadiolyticBase(RateExpr):
    pass  # for isinstance checks


@memoize(None)
def mk_Radiolytic(*doserate_names):
    """Create a Radiolytic rate expression

    Note that there is no mass-action dependence in the resulting
    class, i.e. the rates does not depend on any concentrations.

    Parameters
    ----------
    \\*doserate_names : str instances
        Default: ('',)


    Examples
    --------
    >>> RadiolyticAlpha = mk_Radiolytic('alpha')
    >>> RadiolyticGamma = mk_Radiolytic('gamma')
    >>> dihydrogen_alpha = RadiolyticAlpha([0.8e-7])
    >>> dihydrogen_gamma = RadiolyticGamma([0.45e-7])
    >>> RadiolyticAB = mk_Radiolytic('alpha', 'beta')

    Notes
    -----
    The instance __call__ will require by default ``'density'`` and ``'doserate'``
    in variables.

    """
    if len(doserate_names) == 0:
        doserate_names = ("",)

    class _Radiolytic(RadiolyticBase):
        argument_names = tuple(
            "radiolytic_yield{0}".format("" if drn == "" else "_" + drn)
            for drn in doserate_names
        )  # [amount/energy]
        parameter_keys = ("density",) + tuple(
            "doserate{0}".format("" if drn == "" else "_" + drn)
            for drn in doserate_names
        )

        def args_dimensionality(self, reaction):
            N = base_registry["amount"]
            E = get_derived_unit(base_registry, "energy")
            return (dict(zip(dimension_codes, N / E)),) * self.nargs

        def g_values(self, *args, **kwargs):
            return OrderedDict(
                zip(self.parameter_keys[1:], self.all_args(*args, **kwargs))
            )

        @deprecated(use_instead="Radiolytic.all_args")
        def g_value(self, variables, backend=math, **kwargs):
            (g_val,) = self.all_args(variables, backend=backend, **kwargs)
            return g_val

        def __call__(self, variables, backend=math, reaction=None, **kwargs):
            return variables["density"] * reduce(
                add,
                [
                    variables[k] * gval
                    for k, gval in zip(
                        self.parameter_keys[1:],
                        self.all_args(variables, backend=backend, **kwargs),
                    )
                ],
            )

    _Radiolytic.__name__ = (
        "Radiolytic"
        if doserate_names == ("",)
        else ("Radiolytic_" + "_".join(doserate_names))
    )
    return _Radiolytic


Radiolytic = mk_Radiolytic()


class MassAction(RateExpr, UnaryWrapper):
    """Rate-expression of mass-action type

    Notes
    -----
    :meth:`__call__` requires a :class:`Reaction` instance to be passed as ``reaction``
    keyword argument.

    Examples
    --------
    >>> ma = MassAction([3.14])
    >>> from chempy import Reaction
    >>> r = Reaction.from_string('3 A -> B', param=ma)
    >>> r.rate({'A': 2}) == {'A': -75.36, 'B': 25.12}
    True

    """

    def _str(self, *args, **kwargs):
        (arg,) = self.args
        if isinstance(arg, Symbol):
            (uk,) = arg.unique_keys
            return "'%s'" % uk
        else:
            return super(MassAction, self)._str(*args, **kwargs)

    def __repr__(self):
        return super(MassAction, self)._str(repr)

    def get_named_keys(self):
        # Symbol uses args[0] to return from variables
        (arg,) = self.args
        if isinstance(arg, Symbol):
            return arg.args
        else:
            return self.unique_keys

    argument_names = ("rate_constant",)

    def args_dimensionality(self, reaction):
        order = reaction.order()
        return ({"time": -1, "amount": 1 - order, "length": 3 * (order - 1)},)

    def active_conc_prod(self, variables, backend=math, reaction=None):
        result = 1
        for k, v in reaction.reac.items():
            result = result * variables[k] ** v
        return result

    def rate_coeff(self, variables, backend=math, **kwargs):
        (rat_c,) = self.all_args(variables, backend=backend, **kwargs)
        return rat_c

    def __call__(self, variables, backend=math, reaction=None, **kwargs):
        return self.rate_coeff(
            variables, backend=backend, reaction=reaction
        ) * self.active_conc_prod(
            variables, backend=backend, reaction=reaction, **kwargs
        )

    def string(self, *args, **kwargs):
        if self.args is None and len(self.unique_keys) == 1:
            return self.unique_keys[0]
        else:
            return super(MassAction, self).string(*args, **kwargs)

    @classmethod
    @deprecated(use_instead="MassAction.from_callback")
    def subclass_from_callback(cls, cb, cls_attrs=None):
        """Override MassAction.__call__"""
        _RateExpr = super(MassAction, cls).subclass_from_callback(
            cb, cls_attrs=cls_attrs
        )

        def wrapper(*args, **kwargs):
            obj = _RateExpr(*args, **kwargs)
            return cls(obj)

        return wrapper

    @classmethod
    def from_callback(cls, callback, attr="__call__", **kwargs):
        Wrapper = RateExpr.from_callback(callback, attr=attr, **kwargs)
        return lambda *args, **kwargs: MassAction(Wrapper(*args, **kwargs))


class Arrhenius(Expr):
    """Rate expression for a Arrhenius-type of rate: c0*exp(-c1/T)

    Examples
    --------
    >>> from math import exp
    >>> from chempy import Reaction
    >>> from chempy.units import allclose, default_units as u
    >>> A = 1e11 / u.second
    >>> Ea_over_R = 42e3/8.3145 * u.K**-1
    >>> ratex = MassAction(Arrhenius([A, Ea_over_R]))
    >>> rxn = Reaction({'R'}, {'P'}, ratex)
    >>> dRdt = rxn.rate({'R': 3*u.M, 'temperature': 298.15*u.K})['R']
    >>> assert allclose(dRdt, -3*1e11*exp(-42e3/8.3145/298.15)*u.M/u.s)
    """

    argument_names = ("A", "Ea_over_R")
    parameter_keys = ("temperature",)

    def args_dimensionality(self, reaction):
        order = reaction.order()
        return (
            {"time": -1, "amount": 1 - order, "length": 3 * (order - 1)},
            {"temperature": 1},
        )

    def __call__(self, variables, backend=math, **kwargs):
        A, Ea_over_R = self.all_args(variables, backend=backend, **kwargs)
        try:
            Ea_over_R = Ea_over_R.simplified
        except AttributeError:
            pass
        return A * backend.exp(-Ea_over_R / variables["temperature"])


class Eyring(Expr):
    """Rate expression for Eyring eq: c0*T*exp(-c1/T)

    Note that choice of standard state (c^0) will matter for order > 1.
    """

    argument_names = ("kB_h_times_exp_dS_R", "dH_over_R", "conc0")
    argument_defaults = (1 * _molar,)
    parameter_keys = ("temperature",)

    def args_dimensionality(self, reaction):
        order = reaction.order()
        return (
            {
                "time": -1,
                "temperature": -1,
                "amount": 1 - order,
                "length": 3 * (order - 1),
            },
            {"temperature": 1},
            concentration,
        )

    def __call__(self, variables, backend=math, **kwargs):
        c0, c1, conc0 = self.all_args(variables, backend=backend, **kwargs)
        T = variables["temperature"]
        return c0 * T * backend.exp(-c1 / T) * conc0 ** (1 - kwargs["reaction"].order())


class EyringHS(Expr):
    argument_names = ("dH", "dS", "c0")
    argument_defaults = (1 * _molar,)
    parameter_keys = (
        "temperature",
        "molar_gas_constant",
        "Boltzmann_constant",
        "Planck_constant",
    )

    def args_dimensionality(self, **kwargs):
        return (
            energy + {"amount": -1},
            energy + {"amount": -1, "temperature": -1},
            concentration,
        )

    def __call__(self, variables, backend=math, reaction=None, **kwargs):
        dH, dS, c0 = self.all_args(variables, backend=backend, **kwargs)
        T, R, kB, h = [variables[k] for k in self.parameter_keys]
        return (
            kB
            / h
            * T
            * backend.exp(-(dH - T * dS) / (R * T))
            * c0 ** (1 - reaction.order())
        )


class RampedTemp(Expr):
    """Ramped temperature, pass as substitution to e.g. ``get_odesys``"""

    argument_names = ("T0", "dTdt")
    parameter_keys = ("time",)  # consider e.g. a parameter such as 'init_time'

    def args_dimensionality(self, **kwargs):
        return ({"temperature": 1}, {"temperature": 1, "time": -1})

    def __call__(self, variables, backend=None, **kwargs):
        T0, dTdt = self.all_args(variables, backend=backend, **kwargs)
        return T0 + dTdt * variables["time"]


class SinTemp(Expr):
    argument_names = ("Tbase", "Tamp", "angvel", "phase")
    parameter_keys = ("time",)

    def args_dimensionality(self, **kwargs):
        return ({"temperature": 1}, {"temperature": 1}, {"time": -1}, {})

    def __call__(self, variables, backend=math, **kwargs):
        Tbase, Tamp, angvel, phase = self.all_args(variables, backend=backend, **kwargs)
        return Tbase + Tamp * backend.sin(angvel * variables["time"] + phase)
