# -*- coding: utf-8 -*-

"""Caching decorator for memoizing function results."""

import threading
from collections import OrderedDict
from functools import wraps
from typing import Any, Callable, Optional, Tuple


class _CacheWrapper:
    """Wrapper class to maintain cache for decorated function."""

    def __init__(self, fcn: Callable, maxsize: Optional[int] = None) -> None:
        self.fcn = fcn
        self.cache: OrderedDict[Tuple[Any, ...], Any] = OrderedDict()
        self._lock = threading.Lock()
        self.maxsize = maxsize
        wraps(fcn)(self)

    def __call__(self, *args: Any, **kwargs: Any) -> Any:
        cache_key = (args, tuple(sorted(kwargs.items())))
        with self._lock:
            if cache_key in self.cache:
                self.cache.move_to_end(cache_key)
                return self.cache[cache_key]

            self.cache[cache_key] = self.fcn(*args, **kwargs)

            if self.maxsize is not None and len(self.cache) > self.maxsize:
                self.cache.popitem(last=False)

            return self.cache[cache_key]


def cache(
    fcn: Optional[Callable] = None,
    *,
    maxsize: Optional[int] = None,
) -> Callable:
    """
    Memoizing decorator that caches previous call results to avoid
    redundant computation.

    :func:`functools.lru_cache` is order-sensitive for keyword arguments:
    ``f(a=1, b=2)`` and ``f(b=2, a=1)`` are treated as distinct cache
    entries and the underlying function is called twice. This decorator
    normalises kwargs by sorting them before building the key, so both
    calls map to the same entry.

    If kwargs order-insensitivity is not needed, prefer
    ``@functools.lru_cache(maxsize=N)``, it is implemented in C and
    exposes ``.cache_info()`` / ``.cache_clear()``. Use this decorator
    when you need to memoize functions whose callers may pass the same
    keyword arguments in different orders.

    :param fcn: The function being decorated.

    :param maxsize:
        Maximum number of entries to keep. When the limit is reached the
        least-recently-used entry is evicted. ``None`` (default) means
        the cache is unbounded.

    :return: The wrapped function.
    :rtype: Callable
    """

    def decorator(_fcn: Callable) -> _CacheWrapper:
        return _CacheWrapper(_fcn, maxsize=maxsize)

    if fcn is not None:
        return decorator(fcn)

    return decorator
