# Module responsible for the objects inventory.
#
# Credits to Brian Skinn and the sphobjinv project:
# https://github.com/bskinn/sphobjinv.

from __future__ import annotations

import re
import zlib
from textwrap import dedent
from typing import TYPE_CHECKING, BinaryIO, Literal, overload

if TYPE_CHECKING:
    from collections.abc import Collection


class InventoryItem:
    """Inventory item."""

    def __init__(
        self,
        name: str,
        domain: str,
        role: str,
        uri: str,
        priority: int = 1,
        dispname: str | None = None,
    ):
        """Initialize the object.

        Arguments:
            name: The item name.
            domain: The item domain, like 'python' or 'crystal'.
            role: The item role, like 'class' or 'method'.
            uri: The item URI.
            priority: The item priority. Only used internally by mkdocstrings and Sphinx.
            dispname: The item display name.
        """
        self.name: str = name
        """The item name."""
        self.domain: str = domain
        """The item domain."""
        self.role: str = role
        """The item role."""
        self.uri: str = uri
        """The item URI."""
        self.priority: int = priority
        """The item priority."""
        self.dispname: str = dispname or name
        """The item display name."""

    def format_sphinx(self) -> str:
        """Format this item as a Sphinx inventory line.

        Returns:
            A line formatted for an `objects.inv` file.
        """
        dispname = self.dispname
        if dispname == self.name:
            dispname = "-"
        uri = self.uri
        if uri.endswith(self.name):
            uri = uri[: -len(self.name)] + "$"
        return f"{self.name} {self.domain}:{self.role} {self.priority} {uri} {dispname}"

    sphinx_item_regex = re.compile(r"^(.+?)\s+(\S+):(\S+)\s+(-?\d+)\s+(\S+)\s*(.*)$")
    """Regex to parse a Sphinx v2 inventory line."""

    @overload
    @classmethod
    def parse_sphinx(cls, line: str, *, return_none: Literal[False]) -> InventoryItem: ...

    @overload
    @classmethod
    def parse_sphinx(cls, line: str, *, return_none: Literal[True]) -> InventoryItem | None: ...

    @classmethod
    def parse_sphinx(cls, line: str, *, return_none: bool = False) -> InventoryItem | None:
        """Parse a line from a Sphinx v2 inventory file and return an `InventoryItem` from it."""
        match = cls.sphinx_item_regex.search(line)
        if not match:
            if return_none:
                return None
            raise ValueError(line)
        name, domain, role, priority, uri, dispname = match.groups()
        if uri.endswith("$"):
            uri = uri[:-1] + name
        if dispname == "-":
            dispname = name
        return cls(name, domain, role, uri, int(priority), dispname)


class Inventory(dict):
    """Inventory of collected and rendered objects."""

    def __init__(self, items: list[InventoryItem] | None = None, project: str = "project", version: str = "0.0.0"):
        """Initialize the object.

        Arguments:
            items: A list of items.
            project: The project name.
            version: The project version.
        """
        super().__init__()
        items = items or []
        for item in items:
            self[item.name] = item
        self.project = project
        """The project name."""
        self.version = version
        """The project version."""

    def register(
        self,
        name: str,
        domain: str,
        role: str,
        uri: str,
        priority: int = 1,
        dispname: str | None = None,
    ) -> None:
        """Create and register an item.

        Arguments:
            name: The item name.
            domain: The item domain, like 'python' or 'crystal'.
            role: The item role, like 'class' or 'method'.
            uri: The item URI.
            priority: The item priority. Only used internally by mkdocstrings and Sphinx.
            dispname: The item display name.
        """
        self[name] = InventoryItem(
            name=name,
            domain=domain,
            role=role,
            uri=uri,
            priority=priority,
            dispname=dispname,
        )

    def format_sphinx(self) -> bytes:
        """Format this inventory as a Sphinx `objects.inv` file.

        Returns:
            The inventory as bytes.
        """
        header = (
            dedent(
                f"""
                # Sphinx inventory version 2
                # Project: {self.project}
                # Version: {self.version}
                # The remainder of this file is compressed using zlib.
                """,
            )
            .lstrip()
            .encode("utf8")
        )

        lines = [
            item.format_sphinx().encode("utf8")
            for item in sorted(self.values(), key=lambda item: (item.domain, item.name))
        ]
        return header + zlib.compress(b"\n".join(lines) + b"\n", 9)

    @classmethod
    def parse_sphinx(cls, in_file: BinaryIO, *, domain_filter: Collection[str] = ()) -> Inventory:
        """Parse a Sphinx v2 inventory file and return an `Inventory` from it.

        Arguments:
            in_file: The binary file-like object to read from.
            domain_filter: A collection of domain values to allow (and filter out all other ones).

        Returns:
            An inventory containing the collected items.
        """
        in_file.readline()  # "Sphinx inventory version X"
        project = in_file.readline().removeprefix(b"# Project: ").decode("utf8").rstrip()
        version = in_file.readline().removeprefix(b"# Version: ").decode("utf8").rstrip()
        in_file.readline()  # "The remainder of this file is compressed using zlib"

        lines = zlib.decompress(in_file.read()).splitlines()
        items: list[InventoryItem] = [
            item for line in lines if (item := InventoryItem.parse_sphinx(line.decode("utf8"), return_none=True))
        ]
        if domain_filter:
            items = [item for item in items if item.domain in domain_filter]
        return cls(items, project=project, version=version)
