# Copyright 2009-2022 Joshua Bronson. All rights reserved.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

"""Provide :func:`bidict.namedbidict`."""

import typing as t
from sys import _getframe

from ._base import BidictBase
from ._bidict import bidict
from ._typing import KT, VT


# pyright: reportPrivateUsage=false, reportUnnecessaryIsInstance=false


class NamedBidictBase:
    """Base class that namedbidicts derive from."""


def namedbidict(
    typename: str,
    keyname: str,
    valname: str,
    *,
    base_type: t.Type[BidictBase[KT, VT]] = bidict,
) -> t.Type[BidictBase[KT, VT]]:
    r"""Create a new subclass of *base_type* with custom accessors.

    Like :func:`collections.namedtuple` for bidicts.

    The new class's ``__name__`` and ``__qualname__`` will be set to *typename*,
    and its ``__module__`` will be set to the caller's module.

    Instances of the new class will provide access to their
    :attr:`inverse <BidirectionalMapping.inverse>` instances
    via the custom *keyname*\_for property,
    and access to themselves
    via the custom *valname*\_for property.

    *See also* the :ref:`namedbidict usage documentation
    <other-bidict-types:\:func\:\`~bidict.namedbidict\`>`
    (https://bidict.rtfd.io/other-bidict-types.html#namedbidict)

    :raises ValueError: if any of the *typename*, *keyname*, or *valname*
        strings is not a valid Python identifier, or if *keyname == valname*.

    :raises TypeError: if *base_type* is not a :class:`bidict.BidictBase` subclass.
        Any of the concrete bidict types pictured in the
        :ref:`other-bidict-types:Bidict Types Diagram` may be provided
        (https://bidict.rtfd.io/other-bidict-types.html#bidict-types-diagram).
    """
    if not issubclass(base_type, BidictBase):
        raise TypeError(f'{base_type} is not a BidictBase subclass')
    names = (typename, keyname, valname)
    if not all(map(str.isidentifier, names)) or keyname == valname:
        raise ValueError(names)

    basename = base_type.__name__
    get_keyname = property(lambda self: keyname, doc='The keyname of this namedbidict.')
    get_valname = property(lambda self: valname, doc='The valname of this namedbidict.')
    val_by_key_name = f'{valname}_for'
    key_by_val_name = f'{keyname}_for'
    val_by_key_doc = f'{typename} forward {basename}: {keyname} -> {valname}'
    key_by_val_doc = f'{typename} inverse {basename}: {valname} -> {keyname}'
    get_val_by_key = property(lambda self: self, doc=val_by_key_doc)
    get_key_by_val = property(lambda self: self.inverse, doc=key_by_val_doc)

    class NamedBidict(base_type, NamedBidictBase):  # type: ignore [valid-type,misc]  # https://github.com/python/mypy/issues/5865
        """NamedBidict."""

        keyname = get_keyname
        valname = get_valname

        @classmethod
        def _inv_cls_dict_diff(cls) -> t.Dict[str, t.Any]:
            base_diff = super()._inv_cls_dict_diff()
            return {
                **base_diff,
                'keyname': get_valname,
                'valname': get_keyname,
                val_by_key_name: get_key_by_val,
                key_by_val_name: get_val_by_key,
            }

    NamedInv = NamedBidict._inv_cls
    assert NamedInv is not NamedBidict, 'namedbidict classes are not their own inverses'
    setattr(NamedBidict, val_by_key_name, get_val_by_key)
    setattr(NamedBidict, key_by_val_name, get_key_by_val)
    NamedBidict.__name__ = NamedBidict.__qualname__ = typename
    NamedInv.__name__ = NamedInv.__qualname__ = f'{typename}Inv'
    NamedBidict.__doc__ = f'NamedBidict({basename}) {typename!r}: {keyname} -> {valname}'
    NamedInv.__doc__ = f'NamedBidictInv({basename}) {typename!r}: {valname} -> {keyname}'
    caller_module = _getframe(1).f_globals.get('__name__', '__main__')
    NamedBidict.__module__ = NamedInv.__module__ = caller_module
    return NamedBidict  # pyright: ignore [reportUnknownVariableType]
