"""Transformdict: a mapping that transforms keys on lookup

This module and ``test_transformdict.py`` were extracted from a
patch contributed to Python by Antoine Pitrou implementing his
PEP 455 -- Adding a key-transforming dictionary to collections.
That PEP was rejected, and the patch was never merged to CPython.
The original code is in ``transformdict3.patch``, part of
issue #18986: Add a case-insensitive case-preserving dict.

http://bugs.python.org/issue18986
"""

from collections.abc import MutableMapping


_sentinel = object()


class TransformDict(MutableMapping):
    """Dictionary that calls a transformation function when looking
    up keys, but preserves the original keys.

    >>> d = TransformDict(str.lower)
    >>> d['Foo'] = 5
    >>> d['foo'] == d['FOO'] == d['Foo'] == 5
    True
    >>> set(d.keys())
    {'Foo'}
    """

    __slots__ = ('_transform', '_original', '_data')

    def __init__(self, transform, init_dict=None, **kwargs):
        """Create a new TransformDict with the given *transform* function.
        *init_dict* and *kwargs* are optional initializers, as in the
        dict constructor.
        """
        if not callable(transform):
            raise TypeError(
                f'expected a callable, got {transform.__class__!r}')
        self._transform = transform
        # transformed => original
        self._original = {}
        self._data = {}
        if init_dict:
            self.update(init_dict)
        if kwargs:
            self.update(kwargs)

    def getitem(self, key):
        """D.getitem(key) -> (stored key, value)"""
        transformed = self._transform(key)
        original = self._original[transformed]
        value = self._data[transformed]
        return original, value

    @property
    def transform_func(self):
        """This is TransformDict's transformation function"""
        return self._transform

    # Minimum set of methods required for MutableMapping

    def __len__(self):
        return len(self._data)

    def __iter__(self):
        return iter(self._original.values())

    def __getitem__(self, key):
        return self._data[self._transform(key)]

    def __setitem__(self, key, value):
        transformed = self._transform(key)
        self._data[transformed] = value
        self._original.setdefault(transformed, key)

    def __delitem__(self, key):
        transformed = self._transform(key)
        del self._data[transformed]
        del self._original[transformed]

    # Methods overridden to mitigate the performance overhead.

    def clear(self):
        """D.clear() -> None.  Remove all items from D."""
        self._data.clear()
        self._original.clear()

    def __contains__(self, key):
        return self._transform(key) in self._data

    def get(self, key, default=None):
        """D.get(k[,d]) -> D[k] if k in D, else d.  d defaults to None."""
        return self._data.get(self._transform(key), default)

    def pop(self, key, default=_sentinel):
        """D.pop(k[,d]) -> v, remove key and return corresponding value.
           If key is not found, d is returned if given, otherwise
           KeyError is raised.
        """
        transformed = self._transform(key)
        if default is _sentinel:
            del self._original[transformed]
            return self._data.pop(transformed)
        else:
            self._original.pop(transformed, None)
            return self._data.pop(transformed, default)

    def popitem(self):
        """D.popitem() -> (k, v), remove and return some (key, value) pair
           as a 2-tuple; but raise KeyError if D is empty.
        """
        transformed, value = self._data.popitem()
        return self._original.pop(transformed), value

    # Other methods

    def copy(self):
        """D.copy() -> a shallow copy of D"""
        other = self.__class__(self._transform)
        other._original = self._original.copy()
        other._data = self._data.copy()
        return other

    __copy__ = copy

    def __getstate__(self):
        return (self._transform, self._data, self._original)

    def __setstate__(self, state):
        self._transform, self._data, self._original = state

    def __repr__(self):
        try:
            equiv = dict(self)
        except TypeError:
            # Some keys are unhashable, fall back on .items()
            equiv = list(self.items())
        return f'{self.__class__.__name__}({self._transform!r}, {equiv!r})'
