# -*- coding: utf-8 -*-
# Copyright 2009-2020 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 ._abc import BidirectionalMapping, KT, VT
from ._bidict import bidict


def namedbidict(
    typename: str,
    keyname: str,
    valname: str,
    *,
    base_type: _t.Type[BidirectionalMapping[KT, VT]] = bidict,
) -> _t.Type[BidirectionalMapping[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\`>`

    :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:`BidirectionalMapping` subclass
        that provides ``_isinv`` and :meth:`~object.__getstate__` attributes.
        (Any :class:`~bidict.BidictBase` subclass can be passed in, including all the
        concrete bidict types pictured in the :ref:`other-bidict-types:Bidict Types Diagram`.
    """
    if not issubclass(base_type, BidirectionalMapping) or not all(hasattr(base_type, i) for i in ('_isinv', '__getstate__')):
        raise TypeError(base_type)
    names = (typename, keyname, valname)
    if not all(map(str.isidentifier, names)) or keyname == valname:
        raise ValueError(names)

    class _Named(base_type):  # type: ignore

        __slots__ = ()

        def _getfwd(self) -> '_Named':
            return self.inverse if self._isinv else self  # type: ignore

        def _getinv(self) -> '_Named':
            return self if self._isinv else self.inverse  # type: ignore

        @property
        def _keyname(self) -> str:
            return valname if self._isinv else keyname

        @property
        def _valname(self) -> str:
            return keyname if self._isinv else valname

        def __reduce__(self) -> '_t.Tuple[_t.Callable[[str, str, str, _t.Type[BidirectionalMapping]], BidirectionalMapping], _t.Tuple[str, str, str, _t.Type[BidirectionalMapping]], dict]':
            return (_make_empty, (typename, keyname, valname, base_type), self.__getstate__())

    bname = base_type.__name__
    fname = valname + '_for'
    iname = keyname + '_for'
    fdoc = f'{typename} forward {bname}: {keyname} → {valname}'
    idoc = f'{typename} inverse {bname}: {valname} → {keyname}'
    setattr(_Named, fname, property(_Named._getfwd, doc=fdoc))
    setattr(_Named, iname, property(_Named._getinv, doc=idoc))

    _Named.__name__ = typename
    _Named.__qualname__ = typename
    _Named.__module__ = _getframe(1).f_globals.get('__name__')  # type: ignore
    return _Named


def _make_empty(
    typename: str,
    keyname: str,
    valname: str,
    base_type: _t.Type[BidirectionalMapping] = bidict,
) -> BidirectionalMapping:
    """Create a named bidict with the indicated arguments and return an empty instance.
    Used to make :func:`bidict.namedbidict` instances picklable.
    """
    cls = namedbidict(typename, keyname, valname, base_type=base_type)
    return cls()
