# 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.

"""
View extension to model page descriptions.

A Place describes information about a page that can be used to generate
elements like page titles and breadcrumb elements.
"""

import abc
from enum import StrEnum
from typing import Any, NotRequired, TypedDict, Unpack, override

from django.utils.html import format_html
from django.utils.safestring import SafeString


class PlaceType(StrEnum):
    """Supported place types."""

    INDEX = "index"
    RESOURCE = "resource"
    CONTAINER = "container"


class PlaceKwargs(TypedDict):
    """Argument typing for Place constructor."""

    #: Title
    title: str
    #: Description
    description: NotRequired[str | None]
    #: URL
    url: str
    #: Icon
    icon: NotRequired[str | None]
    #: Parent place (used for breadcrumbs)
    parent: NotRequired["BreadcrumbPlace | None"]


class Place(abc.ABC):
    """Describe a page in Debusine's web space."""

    def __init__(self, **kwargs: Unpack[PlaceKwargs]) -> None:
        """Store Place attributes."""
        #: Title
        self.title = kwargs["title"]
        #: Description
        self.description = kwargs.get("description")
        #: URL
        self.url = kwargs["url"]
        #: Icon
        self.icon = kwargs.get("icon")
        #: Parent place (used for breadcrumbs)
        self.parent = kwargs.get("parent")

    @abc.abstractmethod
    def get_type(self) -> PlaceType:
        """Get the view type for this place."""

    @abc.abstractmethod
    def is_page_title_visible(self) -> bool:
        """Return True if the page title should be shown."""

    def as_icon(self, **kwargs: Any) -> str:
        """
        Render the icon.

        :returns: the rendered HTML, or an empty string if this Place has no
          icon
        """
        if self.icon is None:
            return SafeString()
        else:
            return format_html(
                """<span class="bi bi-{icon}"></span> """, icon=self.icon
            )

    def as_tooltip(self, **kwargs: Any) -> str:
        """Render as a title= attribute."""
        if self.description is not None:
            return format_html(
                ' title="{description}"', description=self.description
            )
        else:
            return SafeString()

    def as_nav(self, **kwargs: Any) -> str:
        """Render as a <nav> link."""
        return format_html(
            """<a class="nav-link btn btn-debusine" href="{url}"{desc}>"""
            "{icon}{title}</a>",
            url=self.url,
            icon=self.as_icon(),
            title=self.title,
            desc=self.as_tooltip(),
        )

    def as_head_title(self, **kwargs: Any) -> str:
        """Render as a <title> element."""
        if self.parent is None:
            prefix = ""
        else:
            prefix = " - ".join(p.breadcrumb for p in self.parent.parents())
            prefix += " - "
        return format_html(
            "<title>{prefix}{title}</title>", prefix=prefix, title=self.title
        )

    @abc.abstractmethod
    def as_page_title(self, **kwargs: Any) -> str:
        """Render as a page title."""

    @abc.abstractmethod
    def as_breadcrumbs(self, **kwargs: Any) -> str:
        """Render as breadcrumbs leading to this Place."""

    def as_button(self, **kwargs: Any) -> str:
        """
        Render as a button.

        Supported keyword arguments:

        * ``label``: override button label
        """
        # TODO: support a way for templates to pass class information
        return format_html(
            """<a class="btn btn-primary" href="{url}"{tooltip}>"""
            """{icon}{title}</a>""",
            url=self.url,
            title=kwargs.get("label", self.title),
            icon=self.as_icon(),
            tooltip=self.as_tooltip(),
        )


class PlaceWithoutPageTitle(Place):
    """Place which does not render a page title."""

    @override
    def is_page_title_visible(self) -> bool:
        return False

    @override
    def as_page_title(self, **kwargs: Any) -> str:
        return SafeString()


class PlaceWithPageTitle(Place):
    """Place which renders a page title."""

    @override
    def is_page_title_visible(self) -> bool:
        return True

    @override
    def as_page_title(self, **kwargs: Any) -> str:
        return format_html(
            """<h1 class="mb-4">{title}</h1>""", title=self.title
        )


class IndexPlace(PlaceWithoutPageTitle):
    """Place showing an overview or a list of resources."""

    @override
    def get_type(self) -> PlaceType:
        return PlaceType.INDEX

    @override
    def as_breadcrumbs(self, **kwargs: Any) -> str:
        if self.parent:
            parent = self.parent.as_breadcrumb_parent()
        else:
            parent = SafeString()
        return format_html(
            """{parent}<h1 class="breadcrumb-current">{title}</h1>""",
            parent=parent,
            title=self.title,
        )


class BreadcrumbPlaceKwargs(PlaceKwargs):
    """Argument typing for BreadcrumbPlace constructor."""

    #: Short label used for breadcrumbs
    breadcrumb: str


class BreadcrumbPlace(Place, abc.ABC):
    """Place supporting showing in the breadcrumbs bar."""

    def __init__(
        self, *, breadcrumb: str, **kwargs: Unpack[PlaceKwargs]
    ) -> None:
        """Store Place attributes."""
        super().__init__(**kwargs)
        #: Short label used for breadcrumbs
        self.breadcrumb = breadcrumb

    def parents(self, **kwargs: Any) -> list["BreadcrumbPlace"]:
        """Build the parent chain as a list, topmost parent first, self last."""
        if self.parent is None:
            res = []
        else:
            res = self.parent.parents()
        res.append(self)
        return res

    def as_breadcrumb_parent(self, **kwargs: Any) -> str:
        """Render as a parent in a breadcrumb list."""
        if self.parent:
            parent = self.parent.as_breadcrumb_parent()
        else:
            parent = SafeString()
        return format_html(
            """{parent}<a href="{url}" class="breadcrumb-parent"{tooltip}>"""
            """{breadcrumb}</a>"""
            """<span class="breadcrumb-separator"></span>""",
            parent=parent,
            url=self.url,
            tooltip=self.as_tooltip(),
            breadcrumb=self.breadcrumb,
        )


class ResourcePlaceKwargs(BreadcrumbPlaceKwargs):
    """Argument typing for ResourcePlace constructor."""


class ResourcePlace(BreadcrumbPlace, PlaceWithPageTitle):
    """Place showing details about a resource."""

    @override
    def get_type(self) -> PlaceType:
        return PlaceType.RESOURCE

    @override
    def as_breadcrumbs(self, **kwargs: Any) -> str:
        if self.parent:
            parent = self.parent.as_breadcrumb_parent()
        else:
            parent = SafeString()
        return format_html(
            "{parent}"
            """<span class="breadcrumb-current"{tooltip}>{title}</span>""",
            parent=parent,
            tooltip=self.as_tooltip(),
            title=self.breadcrumb,
        )


class ContainerPlaceKwargs(BreadcrumbPlaceKwargs):
    """Argument typing for ContainerPlace constructor."""


class ContainerPlace(BreadcrumbPlace, PlaceWithoutPageTitle):
    """Place showing a group of resources."""

    @override
    def get_type(self) -> PlaceType:
        return PlaceType.CONTAINER

    @override
    def as_breadcrumbs(self, **kwargs: Any) -> str:
        if self.parent:
            parent = self.parent.as_breadcrumb_parent()
        else:
            parent = SafeString()
        return format_html(
            "{parent}"
            """<span class="breadcrumb-current"{tooltip}>{title}</span>""",
            parent=parent,
            tooltip=self.as_tooltip(),
            title=self.title,
        )
