"""
pint.registry
~~~~~~~~~~~~~

Defines the UnitRegistry, a class to contain units and their relations.

This registry contains all pint capabilities, but you can build your
customized registry by picking only the features that you actually
need.

:copyright: 2022 by Pint Authors, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""

from __future__ import annotations

from typing import Generic

from . import facets, registry_helpers
from .compat import TypeAlias
from .util import logger, pi_theorem

# To build the Quantity and Unit classes
# we follow the UnitRegistry bases
# but


class Quantity(
    Generic[facets.MagnitudeT],
    facets.SystemRegistry.Quantity[facets.MagnitudeT],
    facets.ContextRegistry.Quantity[facets.MagnitudeT],
    facets.DaskRegistry.Quantity[facets.MagnitudeT],
    facets.NumpyRegistry.Quantity[facets.MagnitudeT],
    facets.MeasurementRegistry.Quantity[facets.MagnitudeT],
    facets.NonMultiplicativeRegistry.Quantity[facets.MagnitudeT],
    facets.PlainRegistry.Quantity[facets.MagnitudeT],
):
    pass


class Unit(
    facets.SystemRegistry.Unit,
    facets.ContextRegistry.Unit,
    facets.DaskRegistry.Unit,
    facets.NumpyRegistry.Unit,
    facets.MeasurementRegistry.Unit,
    facets.NonMultiplicativeRegistry.Unit,
    facets.PlainRegistry.Unit,
):
    pass


class GenericUnitRegistry(
    Generic[facets.QuantityT, facets.UnitT],
    facets.GenericSystemRegistry[facets.QuantityT, facets.UnitT],
    facets.GenericContextRegistry[facets.QuantityT, facets.UnitT],
    facets.GenericDaskRegistry[facets.QuantityT, facets.UnitT],
    facets.GenericNumpyRegistry[facets.QuantityT, facets.UnitT],
    facets.GenericMeasurementRegistry[facets.QuantityT, facets.UnitT],
    facets.GenericNonMultiplicativeRegistry[facets.QuantityT, facets.UnitT],
    facets.GenericPlainRegistry[facets.QuantityT, facets.UnitT],
):
    pass


class UnitRegistry(GenericUnitRegistry[Quantity[facets.MagnitudeT], Unit]):
    """The unit registry stores the definitions and relationships between units.

    Parameters
    ----------
    filename :
        path of the units definition file to load or line-iterable object.
        Empty string to load the default definition file. (default)
        None to leave the UnitRegistry empty.
    force_ndarray : bool
        convert any input, scalar or not to a numpy.ndarray.
        (Default: False)
    force_ndarray_like : bool
        convert all inputs other than duck arrays to a numpy.ndarray.
        (Default: False)
    default_as_delta :
        In the context of a multiplication of units, interpret
        non-multiplicative units as their *delta* counterparts.
        (Default: False)
    autoconvert_offset_to_baseunit :
        If True converts offset units in quantities are
        converted to their plain units in multiplicative
        context. If False no conversion happens. (Default: False)
    on_redefinition : str
        action to take in case a unit is redefined.
        'warn', 'raise', 'ignore' (Default: 'raise')
    auto_reduce_dimensions :
        If True, reduce dimensionality on appropriate operations.
        (Default: False)
    autoconvert_to_preferred :
        If True, converts preferred units on appropriate operations.
        (Default: False)
    preprocessors :
        list of callables which are iteratively ran on any input expression
        or unit string or None for no preprocessor.
        (Default=None)
    fmt_locale :
        locale identifier string, used in `format_babel` or None.
        (Default=None)
    case_sensitive : bool, optional
        Control default case sensitivity of unit parsing. (Default: True)
    cache_folder : str or pathlib.Path or None, optional
        Specify the folder in which cache files are saved and loaded from.
        If None, the cache is disabled. (default)
    """

    Quantity: TypeAlias = Quantity
    Unit: TypeAlias = Unit

    def __init__(
        self,
        filename="",
        force_ndarray: bool = False,
        force_ndarray_like: bool = False,
        default_as_delta: bool = True,
        autoconvert_offset_to_baseunit: bool = False,
        on_redefinition: str = "warn",
        system=None,
        auto_reduce_dimensions=False,
        autoconvert_to_preferred=False,
        preprocessors=None,
        fmt_locale=None,
        non_int_type=float,
        case_sensitive: bool = True,
        cache_folder=None,
    ):
        super().__init__(
            filename=filename,
            force_ndarray=force_ndarray,
            force_ndarray_like=force_ndarray_like,
            on_redefinition=on_redefinition,
            default_as_delta=default_as_delta,
            autoconvert_offset_to_baseunit=autoconvert_offset_to_baseunit,
            system=system,
            auto_reduce_dimensions=auto_reduce_dimensions,
            autoconvert_to_preferred=autoconvert_to_preferred,
            preprocessors=preprocessors,
            fmt_locale=fmt_locale,
            non_int_type=non_int_type,
            case_sensitive=case_sensitive,
            cache_folder=cache_folder,
        )

    def pi_theorem(self, quantities):
        """Builds dimensionless quantities using the Buckingham π theorem

        Parameters
        ----------
        quantities : dict
            mapping between variable name and units

        Returns
        -------
        list
            a list of dimensionless quantities expressed as dicts

        """
        return pi_theorem(quantities, self)

    def setup_matplotlib(self, enable: bool = True) -> None:
        """Set up handlers for matplotlib's unit support.

        Parameters
        ----------
        enable : bool
            whether support should be enabled or disabled (Default value = True)

        """
        # Delays importing matplotlib until it's actually requested
        from .matplotlib import setup_matplotlib_handlers

        setup_matplotlib_handlers(self, enable)

    wraps = registry_helpers.wraps

    check = registry_helpers.check


class LazyRegistry(Generic[facets.QuantityT, facets.UnitT]):
    def __init__(self, args=None, kwargs=None):
        self.__dict__["params"] = args or (), kwargs or {}

    def __init(self):
        args, kwargs = self.__dict__["params"]
        kwargs["on_redefinition"] = "raise"
        self.__class__ = UnitRegistry
        self.__init__(*args, **kwargs)
        self._after_init()

    def __getattr__(self, item):
        if item == "_on_redefinition":
            return "raise"
        self.__init()
        return getattr(self, item)

    def __setattr__(self, key, value):
        if key == "__class__":
            super().__setattr__(key, value)
        else:
            self.__init()
            setattr(self, key, value)

    def __getitem__(self, item):
        self.__init()
        return self[item]

    def __call__(self, *args, **kwargs):
        self.__init()
        return self(*args, **kwargs)


class ApplicationRegistry:
    """A wrapper class used to distribute changes to the application registry."""

    __slots__ = ["_registry"]

    def __init__(self, registry):
        self._registry = registry

    def get(self):
        """Get the wrapped registry"""
        return self._registry

    def set(self, new_registry):
        """Set the new registry

        Parameters
        ----------
        new_registry : ApplicationRegistry or LazyRegistry or UnitRegistry
            The new registry.

        See Also
        --------
        set_application_registry
        """
        if isinstance(new_registry, type(self)):
            new_registry = new_registry.get()

        if not isinstance(new_registry, (LazyRegistry, UnitRegistry)):
            raise TypeError("Expected UnitRegistry; got %s" % type(new_registry))
        logger.debug(
            "Changing app registry from %r to %r.", self._registry, new_registry
        )
        self._registry = new_registry

    def __getattr__(self, name):
        return getattr(self._registry, name)

    def __setattr__(self, name, value):
        if name in self.__slots__:
            super().__setattr__(name, value)
        else:
            setattr(self._registry, name, value)

    def __dir__(self):
        return dir(self._registry)

    def __getitem__(self, item):
        return self._registry[item]

    def __call__(self, *args, **kwargs):
        return self._registry(*args, **kwargs)

    def __contains__(self, item):
        return self._registry.__contains__(item)

    def __iter__(self):
        return iter(self._registry)
