"""This subpackage contains qupulse's expression logic. The submodule :py:mod:`.expressions.protocol` defines the :py:class:`typing.Protocol`
that expression functionality providers must implement. This allows to substitute the powerful and expressive but slow
default implementation with a faster less expressive backend.

The default implementation is in :py:mod:`.expressions.sympy`.

There is are wrapper classes for finding non-protocol uses of expression in :py:mod:`.expressions.wrapper`. Define
``QUPULSE_EXPRESSION_WRAPPER`` environment variable when running python to wrap all expression usages.
"""

from typing import Type, TypeVar
from numbers import Real
import os

import numpy as np
import sympy as sp

from . import sympy, protocol, wrapper


__all__ = ["Expression", "ExpressionVector", "ExpressionScalar",
           "NonNumericEvaluation", "ExpressionVariableMissingException"]


Expression: Type[protocol.Expression] = sympy.Expression
ExpressionScalar: Type[protocol.ExpressionScalar] = sympy.ExpressionScalar
ExpressionVector: Type[protocol.ExpressionVector] = sympy.ExpressionVector


if os.environ.get('QUPULSE_EXPRESSION_WRAPPER', None):  # pragma: no cover
    Expression, ExpressionScalar, ExpressionVector = wrapper.make_wrappers(sympy.Expression,
                                                                           sympy.ExpressionScalar,
                                                                           sympy.ExpressionVector)


ExpressionLike = TypeVar('ExpressionLike', str, Real, sp.Expr, ExpressionScalar)


class ExpressionVariableMissingException(Exception):
    """An exception indicating that a variable value was not provided during expression evaluation.

    See also:
         qupulse.expressions.Expression
    """

    def __init__(self, variable: str, expression: Expression) -> None:
        super().__init__()
        self.variable = variable
        self.expression = expression

    def __str__(self) -> str:
        return f"Could not evaluate <{self.expression}>: A value for variable <{self.variable}> is missing!"


class NonNumericEvaluation(Exception):
    """An exception that is raised if the result of evaluate_numeric is not a number.

    See also:
        qupulse.expressions.Expression.evaluate_numeric
    """

    def __init__(self, expression: Expression, non_numeric_result, call_arguments):
        self.expression = expression
        self.non_numeric_result = non_numeric_result
        self.call_arguments = call_arguments

    def __str__(self) -> str:
        if isinstance(self.non_numeric_result, np.ndarray):
            dtype = self.non_numeric_result.dtype

            if dtype == np.dtype('O'):
                dtypes = set(map(type, self.non_numeric_result.flat))
                return f"The result of evaluate_numeric is an array with the types {dtypes} which is not purely numeric"
        else:
            dtype = type(self.non_numeric_result)
        return f"The result of evaluate_numeric is of type {dtype} which is not a number"
