cimport scipy.special._ufuncs_cxx
import numpy as np


_sf_error_code_map = {
    # skip 'ok'
    'singular': 1,
    'underflow': 2,
    'overflow': 3,
    'slow': 4,
    'loss': 5,
    'no_result': 6,
    'domain': 7,
    'arg': 8,
    'other': 9
}

_sf_error_action_map = {
    'ignore': 0,
    'warn': 1,
    'raise': 2,
    0: 'ignore',
    1: 'warn',
    2: 'raise'
}


def geterr():
    """Get the current way of handling special-function errors.

    Returns
    -------
    err : dict
        A dictionary with keys "singular", "underflow", "overflow",
        "slow", "loss", "no_result", "domain", "arg", and "other",
        whose values are from the strings "ignore", "warn", and
        "raise". The keys represent possible special-function errors,
        and the values define how these errors are handled.

    See Also
    --------
    seterr : set how special-function errors are handled
    errstate : context manager for special-function error handling
    numpy.geterr : similar numpy function for floating-point errors

    Notes
    -----
    For complete documentation of the types of special-function errors
    and treatment options, see `seterr`.

    Examples
    --------
    By default all errors are ignored.

    >>> import scipy.special as sc
    >>> for key, value in sorted(sc.geterr().items()):
    ...     print("{}: {}".format(key, value))
    ...
    arg: ignore
    domain: ignore
    loss: ignore
    no_result: ignore
    other: ignore
    overflow: ignore
    singular: ignore
    slow: ignore
    underflow: ignore

    """
    err = {}
    for key, code in _sf_error_code_map.items():
        action = sf_error.get_action(code)
        err[key] = _sf_error_action_map[action]
    return err


def seterr(**kwargs):
    """Set how special-function errors are handled.

    Parameters
    ----------
    all : {'ignore', 'warn' 'raise'}, optional
        Set treatment for all type of special-function errors at
        once. The options are:

        - 'ignore' Take no action when the error occurs
        - 'warn' Print a `SpecialFunctionWarning` when the error
          occurs (via the Python `warnings` module)
        - 'raise' Raise a `SpecialFunctionError` when the error
          occurs.

        The default is to not change the current behavior. If
        behaviors for additional categories of special-function errors
        are specified, then ``all`` is applied first, followed by the
        additional categories.
    singular : {'ignore', 'warn', 'raise'}, optional
        Treatment for singularities.
    underflow : {'ignore', 'warn', 'raise'}, optional
        Treatment for underflow.
    overflow : {'ignore', 'warn', 'raise'}, optional
        Treatment for overflow.
    slow : {'ignore', 'warn', 'raise'}, optional
        Treatment for slow convergence.
    loss : {'ignore', 'warn', 'raise'}, optional
        Treatment for loss of accuracy.
    no_result : {'ignore', 'warn', 'raise'}, optional
        Treatment for failing to find a result.
    domain : {'ignore', 'warn', 'raise'}, optional
        Treatment for an invalid argument to a function.
    arg : {'ignore', 'warn', 'raise'}, optional
        Treatment for an invalid parameter to a function.
    other : {'ignore', 'warn', 'raise'}, optional
        Treatment for an unknown error.

    Returns
    -------
    olderr : dict
        Dictionary containing the old settings.

    See Also
    --------
    geterr : get the current way of handling special-function errors
    errstate : context manager for special-function error handling
    numpy.seterr : similar numpy function for floating-point errors

    Examples
    --------
    >>> import scipy.special as sc
    >>> from pytest import raises
    >>> sc.gammaln(0)
    inf
    >>> olderr = sc.seterr(singular='raise')
    >>> with raises(sc.SpecialFunctionError):
    ...     sc.gammaln(0)
    ...
    >>> _ = sc.seterr(**olderr)

    We can also raise for every category except one.

    >>> olderr = sc.seterr(all='raise', singular='ignore')
    >>> sc.gammaln(0)
    inf
    >>> with raises(sc.SpecialFunctionError):
    ...     sc.spence(-1)
    ...
    >>> _ = sc.seterr(**olderr)

    """
    olderr = geterr()

    if 'all' in kwargs.keys():
        action = kwargs.pop('all')
        newkwargs = {key: action for key in _sf_error_code_map.keys()}
        for key, value in kwargs.items():
            newkwargs[key] = value
        kwargs = newkwargs

    for error, action in kwargs.items():
        action = _sf_error_action_map[action]
        code = _sf_error_code_map[error]
        _set_action(code, action)
        scipy.special._ufuncs_cxx._set_action(code, action)

    return olderr


class errstate:
    """Context manager for special-function error handling.

    Using an instance of `errstate` as a context manager allows
    statements in that context to execute with a known error handling
    behavior. Upon entering the context the error handling is set with
    `seterr`, and upon exiting it is restored to what it was before.

    Parameters
    ----------
    kwargs : {all, singular, underflow, overflow, slow, loss, no_result, domain, arg, other}
        Keyword arguments. The valid keywords are possible
        special-function errors. Each keyword should have a string
        value that defines the treatement for the particular type of
        error. Values must be 'ignore', 'warn', or 'other'. See
        `seterr` for details.

    See Also
    --------
    geterr : get the current way of handling special-function errors
    seterr : set how special-function errors are handled
    numpy.errstate : similar numpy function for floating-point errors

    Examples
    --------
    >>> import scipy.special as sc
    >>> from pytest import raises
    >>> sc.gammaln(0)
    inf
    >>> with sc.errstate(singular='raise'):
    ...     with raises(sc.SpecialFunctionError):
    ...         sc.gammaln(0)
    ...
    >>> sc.gammaln(0)
    inf

    We can also raise on every category except one.

    >>> with sc.errstate(all='raise', singular='ignore'):
    ...     sc.gammaln(0)
    ...     with raises(sc.SpecialFunctionError):
    ...         sc.spence(-1)
    ...
    inf

    """
    def __init__(self, **kwargs):
        self.kwargs = kwargs

    def __enter__(self):
        self.oldstate = seterr(**self.kwargs)

    def __exit__(self, exc_type, exc_value, traceback):
        seterr(**self.oldstate)
