# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html


"""
Tools for extending Anki.

A hook takes a function that does not return a value.

A filter takes a function that returns its first argument, optionally
modifying it.
"""

from __future__ import annotations

from collections.abc import Callable
from typing import Any

import decorator

# You can find the definitions in ../tools/genhooks.py
from anki.hooks_gen import *

# Legacy hook handling
##############################################################################

_hooks: dict[str, list[Callable[..., Any]]] = {}


def runHook(hook: str, *args: Any) -> None:
    "Run all functions on hook."
    hookFuncs = _hooks.get(hook, None)
    if hookFuncs:
        for func in hookFuncs:
            try:
                func(*args)
            except Exception:
                hookFuncs.remove(func)
                raise


def runFilter(hook: str, arg: Any, *args: Any) -> Any:
    hookFuncs = _hooks.get(hook, None)
    if hookFuncs:
        for func in hookFuncs:
            try:
                arg = func(arg, *args)
            except Exception:
                hookFuncs.remove(func)
                raise
    return arg


def addHook(hook: str, func: Callable) -> None:
    "Add a function to hook. Ignore if already on hook."
    if not _hooks.get(hook, None):
        _hooks[hook] = []
    if func not in _hooks[hook]:
        _hooks[hook].append(func)


def remHook(hook: Any, func: Any) -> None:
    "Remove a function if is on hook."
    hook = _hooks.get(hook, [])
    if func in hook:
        hook.remove(func)


# Monkey patching
##############################################################################
# Please only use this for prototyping or for when hooks are not practical,
# as add-ons that use monkey patching are more likely to break when Anki is
# updated.
#
# If you call wrap() with pos='around', the original function will not be called
# automatically but can be called with _old().
def wrap(old: Any, new: Any, pos: str = "after") -> Callable:
    "Override an existing function."

    def repl(*args: Any, **kwargs: Any) -> Any:
        if pos == "after":
            old(*args, **kwargs)
            return new(*args, **kwargs)
        elif pos == "before":
            new(*args, **kwargs)
            return old(*args, **kwargs)
        else:
            return new(_old=old, *args, **kwargs)

    def decorator_wrapper(f: Any, *args: Any, **kwargs: Any) -> Any:
        return repl(*args, **kwargs)

    return decorator.decorator(decorator_wrapper)(old)
