# Copyright © The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""
Web UI widgets.

Widgets are a way to have code that can be run from Django templates with
access to the template context.
"""

import abc
from collections.abc import Callable
from typing import Any, Concatenate, Literal, overload, override

from django.template.context import Context


class Widget(abc.ABC):
    """Base class for template-renderable elements."""

    @abc.abstractmethod
    def render(self, context: Context) -> str:
        """Render the element."""


@overload
def widget_method[**P](
    *,
    takes_context: Literal[False],
) -> Callable[
    [Callable[Concatenate[Context, P], str]],
    Callable[Concatenate[Context, P], str],
]: ...


@overload
def widget_method[**P](
    *,
    takes_context: Literal[True],
) -> Callable[[Callable[P, str]], Callable[P, str]]: ...


@overload
def widget_method[**P]() -> Callable[[Callable[P, str]], Callable[P, str]]: ...


def widget_method[**P](
    *,
    takes_context: bool = False,
) -> Callable[[Callable[P, str]], Callable[P, str]]:
    """Decorate a method to be usable as a widget in a template."""

    def decorate(f: Callable[P, str]) -> Callable[P, str]:
        f.do_not_call_in_templates = True  # type: ignore[attr-defined]
        f.debusine_widget = True  # type: ignore[attr-defined]
        f.debusine_takes_context = takes_context  # type: ignore[attr-defined]
        return f

    return decorate


class TemplateWidget(Widget):
    """Widget implementation that uses a template."""

    template_name: str

    def get_template_name(self) -> str:
        """Return the name of the template to use."""
        return self.template_name

    def get_context_data(self) -> dict[str, Any]:
        """Return the template context."""
        return {"widget": self}

    @override
    def render(self, context: Context) -> str:
        assert context.template is not None
        template = context.template.engine.get_template(
            self.get_template_name()
        )
        with context.update(self.get_context_data()):
            return template.render(context)
