""" `Integrator`: ODE solver wrapper to use in qutip's Solver """
import numpy as np

__all__ = ['Integrator', 'IntegratorException']


class IntegratorException(Exception):
    """Error from the ODE solver being unable to integrate with the given
    parameters.

    Example
    -------
    - The solver cannot reach the desired tolerance within the maximum number
    of steps.
    - The step needed to be within desired tolerance is too small.
    """


class Integrator:
    """
    A wrapper around ODE solvers.
    It ensures a common interface for Solver usage.
    It takes and return states as :class:`qutip.core.data.Data`, it may return
    a different data-type than the input type.

    Parameters
    ----------
    system: qutip.QobjEvo
        Quantum system in which states evolve.

    options: dict
        Options for the integrator.

    Class Attributes
    ----------------
    name : str
        The name of the integrator.

    supports_blackbox : bool
        If True, then the integrator calls only ``system.matmul``,
        ``system.matmul_data``, ``system.expect``, ``system.expect_data`` and
        ``isconstant``, ``isoper`` or ``issuper``. This allows the solver using
        the integrator to modify the system in creative ways. In particular,
        the solver may modify the system depending on *both* the time ``t``
        *and* the current ``state`` the system is being applied to.

        If the integrator calls any other methods, set to False.

    supports_time_dependent : bool
        If True, then the integrator supports time dependent systems. If False,
        ``supports_blackbox`` should usually be ``False`` too.

    integrator_options : dict
        A dictionary of options used by the integrator and their default
        values. Once initiated, ``self.options`` will be a dict with the same
        keys, not the full options object passed to the solver. Options' keys
        included here will be supported by the :cls:SolverOdeOptions.
    """
    # Dict of used options and their default values
    integrator_options = {}
    _options = None
    # Can evolve time dependent system
    support_time_dependant = None
    # Whether the integrator used the system QobjEvo as a blackbox
    supports_blackbox = None
    # The name of the integrator
    name = None
    method = ""

    def __init__(self, system, options):
        self.system = system
        self._is_set = False  # get_state can be used and return a valid state.
        self._back = (np.inf, None)
        self._options = self.integrator_options.copy()
        self.options = options
        self._prepare()

    def _prepare(self):
        """
        Initialize the solver
        It should also set the name of the solver to be displayed in Result.
        """
        raise NotImplementedError

    def set_state(self, t, state0):
        """
        Set the state of the ODE solver.

        Parameters
        ----------
        t : float
            Initial time

        state0 : qutip.Data
            Initial state.

        .. note:
            It should set the flags `_is_set` to True.
        """
        raise NotImplementedError

    def integrate(self, t, copy=True):
        """
        Evolve to t.

        Before calling `integrate` for the first time, the initial state should
        be set with `set_state`.

        Parameters
        ----------
        t : float
            Time to integrate to, should be larger than the previous time.

        copy : bool [True]
            Whether to return a copy of the state or the state itself.

        Returns
        -------
        (t, state) : (float, qutip.Data)
            The state of the solver at ``t``.
        """
        raise NotImplementedError

    def mcstep(self, t, copy=True):
        """
        Evolve toward the time ``t``.

        If ``t`` is larger than the present state's ``t``, advance the internal
        state toward ``t``. If  ``t`` is smaller than the present ``t``, but
        larger than the previous one, it does an interpolation step and returns
        the state at that time. When advancing the state, it may return it at a
        time between present time and the asked ``t`` if more efficent for
        subsequent interpolation step.

        Before calling `mcstep` for the first time, the initial state should
        be set with `set_state`.

        Parameters
        ----------
        t : float
            Time to integrate to, should be larger than the previous time. If
            the last integrate call was use with ``step=True``, the time can be
            between the time at the start of the last call and now.

        copy : bool [True]
            Whether to return a copy of the state or the state itself.

        Returns
        -------
        (t, state) : (float, qutip.Data)
            The state of the solver at ``t``. The returned time ``t`` can
            differ from the input time only when ``step=True``.

        .. note:
            The default implementation may be overridden by integrators that
            can provide a more efficient one.
        """
        t_last, state = self.get_state()
        if t > t_last:
            self._back = t_last, state
        elif t > self._back[0]:
            self.set_state(*self._back)
        else:
            raise IntegratorException(
                "`t` is outside the integration range: "
                f"{self._back[0]}..{t_last}."
            )
        return self.integrate(t, copy)

    def get_state(self, copy=True):
        """
        Obtain the state of the solver as a pair (t, state).

        Parameters
        ----------
        copy : bool (True)
            Whether to return the data stored in the Integrator or a copy.

        Returns
        -------
        (t, state) : (float, qutip.Data)
            The state of the solver at ``t``.
        """
        raise NotImplementedError

    def run(self, tlist):
        """
        Integrate the system yielding the state for each times in tlist.

        Parameters
        ----------
        tlist : *list* / *array*
            List of times to yield the state.

        Yields
        ------
        (t, state) : (float, qutip.Data)
            The state of the solver at each ``t`` of tlist.
        """
        for t in tlist[1:]:
            yield self.integrate(t, False)

    def reset(self, hard=False):
        """Reset internal state of the ODE solver."""
        if self._is_set:
            state = self.get_state()
        if hard:
            self._prepare()
        if self._is_set:
            self.set_state(*state)

    def arguments(self, args):
        """
        Change the argument of the system.
        Reset the ODE solver to ensure numerical validity.

        Parameters
        ----------
        args : dict
            New arguments
        """
        self.system.arguments(args)
        self.reset()

    @property
    def options(self):
        # Options should be overwritten by each integrators.
        return self._options

    @options.setter
    def options(self, new_options):
        # This does not apply the new options.
        self._options = {
            **self._options,
            **{
               key: new_options[key]
               for key in self.integrator_options.keys()
               if key in new_options and new_options[key] is not None
            }
        }
