# copyright ############################### #
# This file is part of the Xcoll package.   #
# Copyright (c) CERN, 2026.                 #
# ######################################### #

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Callable, Iterable, TypeVar, Generic

T = TypeVar("T")


def _should_intercept(name: str) -> bool:
    """
    Decide which attribute names should go through hook logic.

    Rule of thumb:
      - intercept “normal” public names
      - do NOT intercept dunder methods/attributes
      - do NOT intercept proxy-private attributes (leading underscore)
    """
    if name.startswith("_"):
        return False
    if name.startswith("__") and name.endswith("__"):
        return False
    return True


def _make_forwarder(name: str) -> Callable[..., Any]:
    """Create a special-method forwarder to the underlying target."""
    def method(self: Proxy[Any], *args: Any, **kwargs: Any) -> Any:
        return getattr(self._target, name)(*args, **kwargs)
    method.__name__ = name
    return method


# A fairly comprehensive (but still finite) list of “common” special methods to forward.
# Add/remove depending on your needs.
_SPECIALS_TO_FORWARD: tuple[str, ...] = (
    # Basic protocol / representation
    "__bytes__",
    "__format__",
    "__str__",

    # Container / iteration
    "__len__",
    "__iter__",
    "__reversed__",
    "__contains__",
    "__getitem__",
    "__setitem__",
    "__delitem__",

    # Callable
    "__call__",

    # Context manager
    "__enter__",
    "__exit__",

    # Comparisons
    "__lt__",
    "__le__",
    "__eq__",
    "__ne__",
    "__gt__",
    "__ge__",
    "__hash__",

    # Numeric / unary
    "__neg__",
    "__pos__",
    "__abs__",
    "__invert__",

    # Numeric / binary
    "__add__",
    "__sub__",
    "__mul__",
    "__matmul__",
    "__truediv__",
    "__floordiv__",
    "__mod__",
    "__pow__",
    "__and__",
    "__or__",
    "__xor__",
    "__lshift__",
    "__rshift__",

    # Reflected numeric / binary
    "__radd__",
    "__rsub__",
    "__rmul__",
    "__rmatmul__",
    "__rtruediv__",
    "__rfloordiv__",
    "__rmod__",
    "__rpow__",
    "__rand__",
    "__ror__",
    "__rxor__",
    "__rlshift__",
    "__rrshift__",

    # In-place numeric / binary
    "__iadd__",
    "__isub__",
    "__imul__",
    "__imatmul__",
    "__itruediv__",
    "__ifloordiv__",
    "__imod__",
    "__ipow__",
    "__iand__",
    "__ior__",
    "__ixor__",
    "__ilshift__",
    "__irshift__",
)


@dataclass
class Proxy(Generic[T]):
    """
    Delegating proxy with interception hooks.

    - Attribute reads:
        - normal lookup first (Proxy's own attributes/methods)
        - if missing, delegate to target
        - if name is "interceptable", call _on_get(name, value)

    - Attribute writes:
        - proxy-private (leading underscore) go to proxy itself
        - dunder names go to proxy itself
        - otherwise forwarded to target, and _on_set(name, value) is called

    - Deletions:
        - analogous to writes via _on_del

    Notes:
      - Special methods are forwarded by defining them on the Proxy class;
        see _SPECIALS_TO_FORWARD below.
      - We do NOT wrap callables; we return the target's bound method directly,
        so introspection/signatures remain those of the target.
    """
    _target: T

    # ===========================================
    # === Hooks to be overwritten by subclass ===
    # ===========================================
    def _on_get(self, name: str, value: Any) -> Any:
        """Called after a forwarded attribute get. May return an altered value."""
        return value

    def _on_set(self, name: str, value: Any) -> Any:
        """Called before a forwarded attribute set. May return a transformed value."""
        return value

    def _on_del(self, name: str) -> None:
        """Called before a forwarded attribute deletion."""
        return None

    # ==========================================
    # === Core attribute access / delegation ===
    # ==========================================
    def __getattribute__(self, name: str) -> Any:
        # Never delegate proxy internals (not strictly needed, but this
        # is more future-proof for preventing recursion)
        if name in {"_target", "_on_get", "_on_set", "_on_del",
                    "__class__", "__dict__"}:
            return object.__getattribute__(self, name)

        try:
            # Try proxy first
            return object.__getattribute__(self, name)
        except AttributeError:
            # Otherwise delegate to target, then intercept
            target = object.__getattribute__(self, "_target")
            value = getattr(target, name)
            hook = object.__getattribute__(self, "_on_get")
            return hook(name, value)

    def __setattr__(self, name: str, value: Any) -> None:
        # Proxy-private and dunder attributes live on the proxy.
        if name.startswith("_") or (name.startswith("__") and name.endswith("__")):
            object.__setattr__(self, name, value)
            return

        # Forward “normal” writes to target, with interception.
        value = self._on_set(name, value)
        setattr(self._target, name, value)

    def __delattr__(self, name: str) -> None:
        if name.startswith("_") or (name.startswith("__") and name.endswith("__")):
            object.__delattr__(self, name)
            return

        self._on_del(name)
        delattr(self._target, name)

    # ---------------------------------------
    # Quality-of-life
    # ---------------------------------------
    def __repr__(self) -> str:
        return f"{type(self).__name__}<{self._target!r}>"

    def __dir__(self) -> list[str]:
        # Merge proxy + target attributes, so IDEs and tab-completion behave well.
        proxy_dir = set(object.__dir__(self))
        target_dir = set(dir(self._target))
        return sorted(proxy_dir | target_dir)


# Inject special-method forwarders onto the Proxy class.
# (These must be on the class for Python to use them in operators/builtins.)
for _name in _SPECIALS_TO_FORWARD:
    if not hasattr(Proxy, _name):
        setattr(Proxy, _name, _make_forwarder(_name))
