from collections import OrderedDict
from typing import Any


# ---------------------------------------------------------------------------
class LRUDict(OrderedDict):
    """
    A dict that can discard least-recently-used items via maximum capacity.

    An item is considered "used" by direct access via [] or get() only,
    not via iterating over the whole collection with items(), for example.

    Expired entries only get purged after insertions or changes, or by
    manually calling purge().
    """

    def __init__(self, *args: Any, maxkeys: int, **kwargs: Any) -> None:
        """
        Same arguments as OrderedDict with 1 addition

        maxkeys: maximum number of keys being kept.
        """
        super().__init__(*args, **kwargs)
        self.maxkeys = maxkeys
        self.purge()

    def purge(self) -> None:
        """
        Pop least used keys until maximum keys is reached.
        """
        overflowing = max(0, len(self) - self.maxkeys)
        for _ in range(overflowing):
            self.popitem(last=False)

    def __getitem__(self, key: Any) -> Any:
        value = super().__getitem__(key)
        try:
            self.move_to_end(key)
        except KeyError:
            pass
        return value

    def __setitem__(self, key: Any, value: Any) -> None:
        super().__setitem__(key, value)
        self.purge()


# --------------------------------------------------------------------------
class MultiKeyDict(dict):
    """
    A basic multikey dict that implements the normal key/value behavior.

    Also maps the __class__.__name__ output to the key, so items can
    be looked up by class name as well.
    """

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        super().__init__(*args, **kwargs)
        self.REGISTRYKEY_CLASSNAME_MAP: dict[Any, Any] = {}

    def add_entry(self, key, value, secondary_keys=()):
        """
        Add new entry to dict as normal, and map secondary keys if relevant.
        """
        self.__setitem__(key, value)

        for secondary_key in secondary_keys:
            if secondary_key == key:
                continue

            # Secondary keys should be unique!
            if secondary_key in self.REGISTRYKEY_CLASSNAME_MAP:
                raise ValueError(f'{secondary_key} is already mapped to {value}!')

            self.REGISTRYKEY_CLASSNAME_MAP[secondary_key] = key

    def __getitem__(self, key):
        """
        Get item from default dict, or check secondary maps if KeyError.
        """
        try:
            return super().__getitem__(key)
        except KeyError as e:
            original_key_error = e

        try:
            class_key = self.REGISTRYKEY_CLASSNAME_MAP[key]
            return super().__getitem__(class_key)
        except KeyError:
            raise original_key_error from None

    def pop(self, key: Any, d: Any = None) -> Any:
        """
        Pop key from dict if and remove any secondary map entries.
        """
        res = super().pop(key)

        keys = []
        for k, v in self.REGISTRYKEY_CLASSNAME_MAP.items():
            if v == key:
                keys.append(k)

        for k in keys:
            self.REGISTRYKEY_CLASSNAME_MAP.pop(k)

        return res

    def copy(self) -> 'MultiKeyDict':
        cp = MultiKeyDict(self)
        cp.REGISTRYKEY_CLASSNAME_MAP = self.REGISTRYKEY_CLASSNAME_MAP.copy()
        return cp


# ----------------------------------------------------------------------
class ViviCounter(dict):
    """
    If no fields value is set, functions as Auto vivifying dict

    If fields is set, new key value is set to dictionary copy
    of fields so setting key and counter incrementation can
    occur in one step.

    fields = (
        'content_length',
        'hit_count',
        'allow',
        'block',
    )
    """

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        self.fields = kwargs.pop('fields', ())
        super().__init__(*args, **kwargs)

    def __getitem__(self, name):
        try:
            return super().__getitem__(name)
        except KeyError:
            if self.fields:
                value = {field: 0 for field in self.fields}
                self[name] = value
            else:
                value = self[name] = type(self)()
            return value


# ----------------------------------------------------------------------
class OrderedClass(type):
    """
    Use as metaclass to preserve method init order
    """

    @classmethod
    def __prepare__(mcs, name, bases, **kwargs):
        return OrderedDict()

    def __new__(mcs, name, bases, namespace, **kwargs):
        result = type.__new__(mcs, name, bases, dict(namespace))
        result.members = tuple(namespace)  # type: ignore[attr-defined]
        return result


__all__ = (
    'MultiKeyDict',
    'ViviCounter',
    'LRUDict',
    'MultiKeyDict',
    'OrderedClass',
)
