"""Provides the base class for Printers"""

from sympy.core.basic import Basic as SympyBasic
from sympy.printing.printer import Printer as SympyPrinter

from ..core.scalar_algebra import Scalar
from ..utils.indices import StrLabel
from ._render_head_repr import render_head_repr
from .sympy import SympyStrPrinter


__all__ = []
__private__ = ['QalgebraBasePrinter']


class QalgebraBasePrinter(SympyPrinter):
    """Base class for all QAlgebra expression printers

    Args:
        cache (dict or None): A dict that maps expressions to strings. It may
            be given during istantiation to use pre-defined strings for
            specific expressions. The cache will be updated as the printer is
            used.
        settings (dict or None): A dict of settings.

    Class Attributes:
        sympy_printer_cls (type): The class that will be instantiated to print
            Sympy expressions
        _default_settings (dict): The default value of all settings. Note only
            settings for which there are defaults defined here are accepted
            when instantiating the printer
        printmethod (None or str): Name of a method that expressions may define
            to print themeselves.

    Attributes:
        cache (dict): Dictionary where the results of any call to
            :meth:`doprint` is stored. When :meth:`doprint` is called for an
            expression that is already in `cache`, the result from the
            cache is returned.
        _sympy_printer (sympy.printing.printer.Printer): The printer instance
            that will be used to print any Sympy expression.
        _allow_caching (bool): A flag that may be set to completely disable
            caching
        _print_level (int): The recursion depth of :meth:`doprint`
            (>= 1 inside any of the ``_print*`` methods)

    Raises:
        TypeError: If any key in `settings` is not defined in the
            `_default_settings` of the printer, respectively the
            `sympy_printer_cls`.
    """

    sympy_printer_cls = SympyStrPrinter

    _default_settings = {}
    # note that subclasses should always define their own _default_settings

    _allow_caching = True

    printmethod = None

    def __init__(self, cache=None, settings=None):
        # This would be more elegant if we declared this as **settings, but we
        # want to closely mirror the SymPy implementation. The frontend-facing
        # routines, e.g. init_printing or ascii do flatten out the settings
        self.cache = {}
        if cache is not None:
            self.cache = cache
        sympy_settings = {}
        qalgebra_settings = {}
        if settings is not None:
            for key in settings:
                key_ok = False
                if key in self.sympy_printer_cls._default_settings:
                    key_ok = True
                    sympy_settings[key] = settings[key]
                if key in self._default_settings:
                    key_ok = True
                    qalgebra_settings[key] = settings[key]
                if not key_ok:
                    raise TypeError(
                        "%s is not a valid setting for either %s or %s"
                        % (
                            key,
                            self.__class__.__name__,
                            self.sympy_printer_cls.__name__,
                        )
                    )
        self._sympy_printer = self.sympy_printer_cls(settings=sympy_settings)
        super().__init__(settings=qalgebra_settings)

    def _render_str(self, string):
        if isinstance(string, StrLabel):
            string = string._render(string.expr)
        return str(string)

    def emptyPrinter(self, expr):
        """Fallback method for expressions that neither know how to print
        themeselves, nor for which the printer has a suitable ``_print*``
        method"""
        return render_head_repr(expr)

    @staticmethod
    def _isinstance(expr, classname):
        """Check whether `expr` is an instance of the class with name
        `classname`

        This is like the builtin `isinstance`, but it take the `classname` a
        string, instead of the class directly. Useful for when we don't want to
        import the class for which we want to check (also, remember that
        printer choose rendering method based on the class name, so this is
        totally ok)
        """
        for cls in type(expr).__mro__:
            if cls.__name__ == classname:
                return True
        return False

    def _get_from_cache(self, expr):
        """Get the result of :meth:`doprint` from the internal cache"""
        # The reason method this is separated out from `doprint` is that
        # printers that use identation, e.g. IndentedSReprPrinter, need to
        # override how caching is handled, applying variable indentation even
        # for cached results
        try:
            is_cached = expr in self.cache
        except TypeError:
            # expr is unhashable
            is_cached = False
        if is_cached:
            return True, self.cache[expr]
        else:
            return False, None

    def _write_to_cache(self, expr, res):
        """Store the result of :meth:`doprint` in the internal cache"""
        # Well be overwritten by printers that use indentation, see
        # _get_from_cache
        try:
            self.cache[expr] = res
        except TypeError:
            # expr is unhashable
            pass

    def _print_SCALAR_TYPES(self, expr, *args, **kwargs):
        """Render scalars"""
        adjoint = kwargs.get('adjoint', False)
        if adjoint:
            expr = expr.conjugate()
        if isinstance(expr, SympyBasic):
            self._sympy_printer._print_level = self._print_level + 1
            res = self._sympy_printer.doprint(expr)
        else:  # numeric type
            try:
                if int(expr) == expr:
                    # In Python, objects that evaluate equal (e.g. 2.0 == 2)
                    # have the same hash. We want to normalize this, so that we
                    # get consistent results when printing with a cache
                    expr = int(expr)
            except TypeError:
                pass
            if adjoint:
                kwargs = {
                    key: val
                    for (key, val) in kwargs.items()
                    if key != 'adjoint'
                }
            res = self._print(expr, *args, **kwargs)
        return res

    def doprint(self, expr, *args, **kwargs):
        """Returns printer's representation for expr (as a string)

        The representation is obtained by the following methods:

        1. from the Printer cache
        2. If `expr` is a Sympy object, delegate to the
           :meth:`~sympy.printing.printer.Printer.doprint` method of
           the SymPy printer referenced by the QAlgebra printer's
           ``_sympy_printer`` class attribute.
        3. Let the `expr` print itself if has the :attr:`printmethod`
        4. Take the best fitting ``_print_*`` method of the printer
        5. As fallback, delegate to :meth:`emptyPrinter`

        Any extra `args` or `kwargs` are passed to the internal `_print`
        method.
        """
        allow_caching = self._allow_caching
        is_cached = False
        if len(args) > 0 or len(kwargs) > 0:
            # we don't want to cache "custom" rendering, such as the adjoint of
            # the actual expression (kwargs['adjoint'] is True). Otherwise, we
            # might return a cached values for args/kwargs that are different
            # from the the expression was originally cached.
            allow_caching = False

        if allow_caching:
            is_cached, res = self._get_from_cache(expr)
        if not is_cached:
            if isinstance(expr, Scalar._val_types):
                res = self._print_SCALAR_TYPES(expr, *args, **kwargs)
            elif isinstance(expr, str):
                return self._render_str(expr)
            else:
                # the _print method, inherited from SympyPrinter implements the
                # internal dispatcher for (3-5)
                res = self._str(self._print(expr, *args, **kwargs))
            if allow_caching:
                self._write_to_cache(expr, res)
        return res
