# gir.py
#
# Copyright 2021 James Westman <james@jwestman.net>
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later

import os
import sys
import typing as T
from functools import cached_property

import gi  # type: ignore

try:
    gi.require_version("GIRepository", "3.0")
    from gi.repository import GIRepository  # type: ignore

    _repo = GIRepository.Repository()
except ValueError:
    # We can remove this once we can bump the minimum dependencies
    # to glib 2.80 and pygobject 3.52
    # dependency('glib-2.0', version: '>= 2.80.0')
    # dependency('girepository-2.0', version: '>= 2.80.0')
    gi.require_version("GIRepository", "2.0")
    from gi.repository import GIRepository  # type: ignore

    _repo = GIRepository.Repository

from . import typelib, xml_reader
from .errors import CompileError, CompilerBugError
from .lsp_utils import CodeAction

_namespace_cache: T.Dict[str, "Namespace"] = {}
_xml_cache = {}

_user_search_paths = []


def add_typelib_search_path(path: str):
    _user_search_paths.append(path)


def get_namespace(namespace: str, version: str) -> "Namespace":
    search_paths = [*_repo.get_search_path(), *_user_search_paths]

    filename = f"{namespace}-{version}.typelib"

    if filename not in _namespace_cache:
        for search_path in search_paths:
            path = os.path.join(search_path, filename)

            if os.path.exists(path) and os.path.isfile(path):
                tl = typelib.load_typelib(path)
                repository = Repository(tl)

                _namespace_cache[filename] = repository.namespace
                break

        if filename not in _namespace_cache:
            raise CompileError(
                f"Namespace {namespace}-{version} could not be found",
                hints=["search path: " + os.pathsep.join(search_paths)],
            )

    return _namespace_cache[filename]


_available_namespaces: list[tuple[str, str]] = []


def get_available_namespaces() -> T.List[T.Tuple[str, str]]:
    if len(_available_namespaces):
        return _available_namespaces

    search_paths: list[str] = [
        *_repo.get_search_path(),
        *_user_search_paths,
    ]

    for search_path in search_paths:
        try:
            filenames = os.listdir(search_path)
        except FileNotFoundError:
            continue

        for filename in filenames:
            if filename.endswith(".typelib"):
                namespace, version = filename.removesuffix(".typelib").rsplit("-", 1)
                _available_namespaces.append((namespace, version))

    return _available_namespaces


def get_xml(namespace: str, version: str):
    search_paths = []

    if data_paths := os.environ.get("XDG_DATA_DIRS"):
        search_paths += [
            os.path.join(path, "gir-1.0") for path in data_paths.split(os.pathsep)
        ]

    filename = f"{namespace}-{version}.gir"

    if filename not in _xml_cache:
        for search_path in search_paths:
            path = os.path.join(search_path, filename)

            if os.path.exists(path) and os.path.isfile(path):
                _xml_cache[filename] = xml_reader.parse(path)
                break

        if filename not in _xml_cache:
            raise CompileError(
                f"GObject introspection file '{namespace}-{version}.gir' could not be found",
                hints=["search path: " + os.pathsep.join(search_paths)],
            )

    return _xml_cache[filename]


ONLINE_DOCS = {
    "Adw-1": "https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/",
    "Gdk-4.0": "https://docs.gtk.org/gdk4/",
    "GdkPixbuf-2.0": "https://docs.gtk.org/gdk-pixbuf/",
    "Gio-2.0": "https://docs.gtk.org/gio/",
    "GLib-2.0": "https://docs.gtk.org/glib/",
    "GModule-2.0": "https://docs.gtk.org/gmodule/",
    "GObject-2.0": "https://docs.gtk.org/gobject/",
    "Gsk-4.0": "https://docs.gtk.org/gsk4/",
    "Gtk-4.0": "https://docs.gtk.org/gtk4/",
    "GtkSource-5": "https://gnome.pages.gitlab.gnome.org/gtksourceview/gtksourceview5",
    "Pango-1.0": "https://docs.gtk.org/Pango/",
    "Shumate-1.0": "https://gnome.pages.gitlab.gnome.org/libshumate/",
    "WebKit2-4.1": "https://webkitgtk.org/reference/webkit2gtk/stable/",
}


class GirType:
    @property
    def doc(self) -> T.Optional[str]:
        return None

    def assignable_to(self, other: "GirType") -> bool:
        raise NotImplementedError()

    @property
    def name(self) -> str:
        """The GIR name of the type, not including the namespace"""
        raise NotImplementedError()

    @property
    def full_name(self) -> str:
        """The GIR name of the type to use in diagnostics"""
        raise NotImplementedError()

    @property
    def glib_type_name(self) -> str:
        """The name of the type in the GObject type system, suitable to pass to `g_type_from_name()`."""
        raise NotImplementedError()

    @property
    def incomplete(self) -> bool:
        return False

    @property
    def deprecated(self) -> bool:
        return False

    @property
    def deprecated_doc(self) -> T.Optional[str]:
        return None


class ExternType(GirType):
    def __init__(self, name: str) -> None:
        super().__init__()
        self._name = name

    def assignable_to(self, other: GirType) -> bool:
        return True

    @property
    def full_name(self) -> str:
        return self._name

    @property
    def glib_type_name(self) -> str:
        return self._name

    @property
    def incomplete(self) -> bool:
        return True


class ArrayType(GirType):
    def __init__(self, inner: GirType) -> None:
        self._inner = inner

    def assignable_to(self, other: GirType) -> bool:
        return isinstance(other, ArrayType) and self._inner.assignable_to(other._inner)

    @property
    def inner(self) -> GirType:
        return self._inner

    @property
    def name(self) -> str:
        return self._inner.name + "[]"

    @property
    def full_name(self) -> str:
        return self._inner.full_name + "[]"


class BasicType(GirType):
    name: str = "unknown type"

    @property
    def full_name(self) -> str:
        return self.name


class BoolType(BasicType):
    name = "bool"
    glib_type_name: str = "gboolean"

    def assignable_to(self, other: GirType) -> bool:
        return isinstance(other, BoolType)


class IntType(BasicType):
    name = "int"
    glib_type_name: str = "gint"

    def assignable_to(self, other: GirType) -> bool:
        return (
            isinstance(other, IntType)
            or isinstance(other, UIntType)
            or isinstance(other, FloatType)
        )


class UIntType(BasicType):
    name = "uint"
    glib_type_name: str = "guint"

    def assignable_to(self, other: GirType) -> bool:
        return (
            isinstance(other, IntType)
            or isinstance(other, UIntType)
            or isinstance(other, FloatType)
        )


class FloatType(BasicType):
    name = "float"
    glib_type_name: str = "gfloat"

    def assignable_to(self, other: GirType) -> bool:
        return isinstance(other, FloatType)


class StringType(BasicType):
    name = "string"
    glib_type_name: str = "gchararray"

    def assignable_to(self, other: GirType) -> bool:
        return isinstance(other, StringType)


class TypeType(BasicType):
    name = "GType"
    glib_type_name: str = "GType"

    def assignable_to(self, other: GirType) -> bool:
        return isinstance(other, TypeType)


_BASIC_TYPES = {
    "bool": BoolType,
    "string": StringType,
    "int": IntType,
    "uint": UIntType,
    "float": FloatType,
    "double": FloatType,
    "type": TypeType,
}


TNode = T.TypeVar("TNode", bound="GirNode")


class GirNode:
    xml_tag: str

    def __init__(self, container: T.Optional["GirNode"], tl: typelib.Typelib) -> None:
        self.container = container
        self.tl = tl

    def get_containing(self, container_type: T.Type[TNode]) -> TNode:
        if self.container is None:
            raise CompilerBugError()
        elif isinstance(self.container, container_type):
            return self.container
        else:
            return self.container.get_containing(container_type)

    @cached_property
    def xml(self):
        for el in self.container.xml.children:
            if el.attrs.get("name") == self.name:
                if el.tag == self.xml_tag:
                    return el

    @cached_property
    def glib_type_name(self) -> str:
        return self.tl.OBJ_GTYPE_NAME

    @cached_property
    def full_name(self) -> str:
        if self.container is None:
            return self.name
        else:
            return f"{self.container.name}.{self.name}"

    @cached_property
    def name(self) -> str:
        return self.tl.BLOB_NAME

    @cached_property
    def cname(self) -> str:
        return self.tl.OBJ_GTYPE_NAME

    @cached_property
    def available_in(self) -> str:
        return self.xml.get("version")

    @cached_property
    def detail(self) -> T.Optional[str]:
        try:
            el = self.xml.get_elements("doc")
            if len(el) == 1:
                return el[0].cdata.strip().partition("\n")[0]
            else:
                return None
        except:
            return None

    @cached_property
    def doc(self) -> T.Optional[str]:
        sections = []

        if self.signature:
            sections.append("```\n" + self.signature + "\n```")

        try:
            el = self.xml.get_elements("doc")
            if len(el) == 1:
                sections.append(el[0].cdata.strip())
        except:
            # Not a huge deal, but if you want docs in the language server you
            # should ensure .gir files are installed
            sections.append("Documentation is not installed")

        if self.online_docs:
            sections.append(f"[Online documentation]({self.online_docs})")

        return "\n\n---\n\n".join(sections)

    @property
    def online_docs(self) -> T.Optional[str]:
        return None

    @property
    def signature(self) -> T.Optional[str]:
        return None

    @property
    def type(self) -> GirType:
        raise NotImplementedError()

    @property
    def deprecated_doc(self) -> T.Optional[str]:
        try:
            return self.xml.get_elements("doc-deprecated")[0].cdata.strip()
        except:
            return None


class Property(GirNode):
    xml_tag = "property"

    def __init__(self, klass: T.Union["Class", "Interface"], tl: typelib.Typelib):
        super().__init__(klass, tl)

    @cached_property
    def name(self) -> str:
        return self.tl.PROP_NAME

    @cached_property
    def type(self):
        return self.get_containing(Repository)._resolve_type_id(self.tl.PROP_TYPE)

    @cached_property
    def signature(self):
        return f"{self.type.full_name} {self.container.name}:{self.name}"

    @property
    def writable(self) -> bool:
        return self.tl.PROP_WRITABLE == 1

    @property
    def construct_only(self) -> bool:
        return self.tl.PROP_CONSTRUCT_ONLY == 1

    @property
    def online_docs(self) -> T.Optional[str]:
        if ns := self.get_containing(Namespace).online_docs:
            assert self.container is not None
            return f"{ns}property.{self.container.name}.{self.name}.html"
        else:
            return None

    @property
    def deprecated(self) -> bool:
        return self.tl.PROP_DEPRECATED == 1


class Argument(GirNode):
    def __init__(self, container: GirNode, tl: typelib.Typelib) -> None:
        super().__init__(container, tl)

    @cached_property
    def name(self) -> str:
        return self.tl.ARG_NAME

    @cached_property
    def type(self) -> GirType:
        return self.get_containing(Repository)._resolve_type_id(self.tl.ARG_TYPE)


class Signature(GirNode):
    def __init__(self, container: GirNode, tl: typelib.Typelib) -> None:
        super().__init__(container, tl)

    @cached_property
    def args(self) -> T.List[Argument]:
        n_arguments = self.tl.SIGNATURE_N_ARGUMENTS
        blob_size = self.tl.header.HEADER_ARG_BLOB_SIZE
        result = []
        for i in range(n_arguments):
            entry = self.tl.SIGNATURE_ARGUMENTS[i * blob_size]
            result.append(Argument(self, entry))
        return result

    @cached_property
    def return_type(self) -> T.Optional[GirType]:
        if self.tl.SIGNATURE_RETURN_TYPE == 0:
            return None
        else:
            return self.get_containing(Repository)._resolve_type_id(
                self.tl.SIGNATURE_RETURN_TYPE
            )


class Signal(GirNode):
    xml_tag = "glib:signal"

    def __init__(
        self, klass: T.Union["Class", "Interface"], tl: typelib.Typelib
    ) -> None:
        super().__init__(klass, tl)

    @cached_property
    def gir_signature(self) -> Signature:
        return Signature(self, self.tl.SIGNAL_SIGNATURE)

    @property
    def signature(self):
        args = ", ".join(
            [f"{a.type.full_name} {a.name}" for a in self.gir_signature.args]
        )
        result = f"signal {self.container.full_name}::{self.name} ({args})"
        if self.gir_signature.return_type is not None:
            result += f" -> {self.gir_signature.return_type.full_name}"
        return result

    @property
    def online_docs(self) -> T.Optional[str]:
        if ns := self.get_containing(Namespace).online_docs:
            assert self.container is not None
            return f"{ns}signal.{self.container.name}.{self.name}.html"
        else:
            return None

    @property
    def deprecated(self) -> bool:
        return self.tl.SIGNAL_DEPRECATED == 1


class Interface(GirNode, GirType):
    xml_tag = "interface"

    def __init__(self, ns: "Namespace", tl: typelib.Typelib):
        super().__init__(ns, tl)

    @cached_property
    def properties(self) -> T.Mapping[str, Property]:
        n_prerequisites = self.tl.INTERFACE_N_PREREQUISITES
        offset = self.tl.header.HEADER_INTERFACE_BLOB_SIZE
        offset += (n_prerequisites + n_prerequisites % 2) * 2
        n_properties = self.tl.INTERFACE_N_PROPERTIES
        property_size = self.tl.header.HEADER_PROPERTY_BLOB_SIZE
        result = {}
        for i in range(n_properties):
            property = Property(self, self.tl[offset + i * property_size])
            result[property.name] = property
        return result

    @cached_property
    def signals(self) -> T.Mapping[str, Signal]:
        n_prerequisites = self.tl.INTERFACE_N_PREREQUISITES
        offset = self.tl.header.HEADER_INTERFACE_BLOB_SIZE
        offset += (n_prerequisites + n_prerequisites % 2) * 2
        offset += (
            self.tl.INTERFACE_N_PROPERTIES * self.tl.header.HEADER_PROPERTY_BLOB_SIZE
        )
        offset += self.tl.INTERFACE_N_METHODS * self.tl.header.HEADER_FUNCTION_BLOB_SIZE
        n_signals = self.tl.INTERFACE_N_SIGNALS
        property_size = self.tl.header.HEADER_SIGNAL_BLOB_SIZE
        result = {}
        for i in range(n_signals):
            signal = Signal(self, self.tl[offset + i * property_size])
            result[signal.name] = signal
        return result

    @cached_property
    def prerequisites(self) -> T.List["Interface"]:
        n_prerequisites = self.tl.INTERFACE_N_PREREQUISITES
        result = []
        for i in range(n_prerequisites):
            entry = self.tl.INTERFACE_PREREQUISITES[i * 2].AS_DIR_ENTRY
            result.append(self.get_containing(Repository)._resolve_dir_entry(entry))
        return result

    def assignable_to(self, other: GirType) -> bool:
        if self == other:
            return True
        for pre in self.prerequisites:
            if pre.assignable_to(other):
                return True
        return False

    @property
    def online_docs(self) -> T.Optional[str]:
        if ns := self.get_containing(Namespace).online_docs:
            return f"{ns}iface.{self.name}.html"
        else:
            return None

    @property
    def deprecated(self) -> bool:
        return self.tl.INTERFACE_DEPRECATED == 1


class Class(GirNode, GirType):
    xml_tag = "class"

    def __init__(self, ns: "Namespace", tl: typelib.Typelib) -> None:
        super().__init__(ns, tl)

    @property
    def abstract(self) -> bool:
        return self.tl.OBJ_ABSTRACT == 1

    @cached_property
    def implements(self) -> T.List[Interface]:
        n_interfaces = self.tl.OBJ_N_INTERFACES
        result = []
        for i in range(n_interfaces):
            entry = self.tl[self.tl.header.HEADER_OBJECT_BLOB_SIZE + i * 2].AS_DIR_ENTRY
            result.append(self.get_containing(Repository)._resolve_dir_entry(entry))
        return result

    @cached_property
    def own_properties(self) -> T.Mapping[str, Property]:
        n_interfaces = self.tl.OBJ_N_INTERFACES
        offset = self.tl.header.HEADER_OBJECT_BLOB_SIZE
        offset += (n_interfaces + n_interfaces % 2) * 2
        offset += self.tl.OBJ_N_FIELDS * self.tl.header.HEADER_FIELD_BLOB_SIZE
        offset += (
            self.tl.OBJ_N_FIELD_CALLBACKS * self.tl.header.HEADER_CALLBACK_BLOB_SIZE
        )
        n_properties = self.tl.OBJ_N_PROPERTIES
        property_size = self.tl.header.HEADER_PROPERTY_BLOB_SIZE
        result = {}
        for i in range(n_properties):
            property = Property(self, self.tl[offset + i * property_size])
            result[property.name] = property
        return result

    @cached_property
    def own_signals(self) -> T.Mapping[str, Signal]:
        n_interfaces = self.tl.OBJ_N_INTERFACES
        offset = self.tl.header.HEADER_OBJECT_BLOB_SIZE
        offset += (n_interfaces + n_interfaces % 2) * 2
        offset += self.tl.OBJ_N_FIELDS * self.tl.header.HEADER_FIELD_BLOB_SIZE
        offset += (
            self.tl.OBJ_N_FIELD_CALLBACKS * self.tl.header.HEADER_CALLBACK_BLOB_SIZE
        )
        offset += self.tl.OBJ_N_PROPERTIES * self.tl.header.HEADER_PROPERTY_BLOB_SIZE
        offset += self.tl.OBJ_N_METHODS * self.tl.header.HEADER_FUNCTION_BLOB_SIZE
        n_signals = self.tl.OBJ_N_SIGNALS
        signal_size = self.tl.header.HEADER_SIGNAL_BLOB_SIZE
        result = {}
        for i in range(n_signals):
            signal = Signal(self, self.tl[offset][i * signal_size])
            result[signal.name] = signal
        return result

    @cached_property
    def parent(self) -> T.Optional["Class"]:
        if entry := self.tl.OBJ_PARENT:
            return self.get_containing(Repository)._resolve_dir_entry(entry)
        else:
            return None

    @cached_property
    def signature(self) -> str:
        assert self.container is not None
        result = f"class {self.container.name}.{self.name}"
        if self.parent is not None:
            assert self.parent.container is not None
            result += f" : {self.parent.container.name}.{self.parent.name}"
        if len(self.implements):
            result += " implements " + ", ".join(
                [impl.full_name for impl in self.implements]
            )
        return result

    @cached_property
    def properties(self) -> T.Mapping[str, Property]:
        return {p.name: p for p in self._enum_properties()}

    @cached_property
    def signals(self) -> T.Mapping[str, Signal]:
        return {s.name: s for s in self._enum_signals()}

    def assignable_to(self, other: GirType) -> bool:
        if self == other:
            return True
        elif self.parent and self.parent.assignable_to(other):
            return True
        else:
            for iface in self.implements:
                if iface.assignable_to(other):
                    return True

            return False

    def _enum_properties(self) -> T.Iterable[Property]:
        yield from self.own_properties.values()

        if self.parent is not None:
            yield from self.parent.properties.values()

        for impl in self.implements:
            yield from impl.properties.values()

    def _enum_signals(self) -> T.Iterable[Signal]:
        yield from self.own_signals.values()

        if self.parent is not None:
            yield from self.parent.signals.values()

        for impl in self.implements:
            yield from impl.signals.values()

    @property
    def online_docs(self) -> T.Optional[str]:
        if ns := self.get_containing(Namespace).online_docs:
            return f"{ns}class.{self.name}.html"
        else:
            return None

    @property
    def deprecated(self) -> bool:
        return self.tl.OBJ_DEPRECATED == 1


class TemplateType(GirType):
    def __init__(self, name: str, parent: T.Optional[GirType]):
        self._name = name
        self.parent = parent

    @property
    def name(self) -> str:
        return self._name

    @property
    def full_name(self) -> str:
        return self._name

    @property
    def glib_type_name(self) -> str:
        return self._name

    @cached_property
    def properties(self) -> T.Mapping[str, Property]:
        if not (isinstance(self.parent, Class) or isinstance(self.parent, Interface)):
            return {}
        else:
            return self.parent.properties

    @cached_property
    def signals(self) -> T.Mapping[str, Signal]:
        if not (isinstance(self.parent, Class) or isinstance(self.parent, Interface)):
            return {}
        else:
            return self.parent.signals

    def assignable_to(self, other: "GirType") -> bool:
        if self == other:
            return True
        elif isinstance(other, Interface):
            # we don't know the template type's interfaces, assume yes
            return True
        elif self.parent is None or isinstance(self.parent, ExternType):
            return isinstance(other, Class) or isinstance(other, ExternType)
        else:
            return self.parent.assignable_to(other)

    @cached_property
    def signature(self) -> str:
        if self.parent is None:
            return f"template {self.name}"
        else:
            return f"template {self.name} : {self.parent.full_name}"

    @property
    def incomplete(self) -> bool:
        return True


class EnumMember(GirNode):
    xml_tag = "member"

    def __init__(self, enum: "Enumeration", tl: typelib.Typelib) -> None:
        super().__init__(enum, tl)

    @property
    def value(self) -> int:
        return self.tl.VALUE_VALUE

    @cached_property
    def name(self) -> str:
        return self.tl.VALUE_NAME

    @cached_property
    def nick(self) -> str:
        return self.name.replace("_", "-")

    @property
    def c_ident(self) -> str:
        return self.tl.attr("c:identifier")

    @property
    def signature(self) -> str:
        return f"enum member {self.full_name} = {self.value}"


class Enumeration(GirNode, GirType):
    xml_tag = "enumeration"

    def __init__(self, ns: "Namespace", tl: typelib.Typelib) -> None:
        super().__init__(ns, tl)

    @cached_property
    def members(self) -> T.Dict[str, EnumMember]:
        members = {}
        n_values = self.tl.ENUM_N_VALUES
        values = self.tl.ENUM_VALUES
        value_size = self.tl.header.HEADER_VALUE_BLOB_SIZE
        for i in range(n_values):
            member = EnumMember(self, values[i * value_size])
            members[member.name] = member
        return members

    @property
    def signature(self) -> str:
        return f"enum {self.full_name}"

    def assignable_to(self, type: GirType) -> bool:
        return type == self

    @property
    def online_docs(self) -> T.Optional[str]:
        if ns := self.get_containing(Namespace).online_docs:
            return f"{ns}enum.{self.name}.html"
        else:
            return None

    @property
    def deprecated(self) -> bool:
        return self.tl.ENUM_DEPRECATED == 1


class Boxed(GirNode, GirType):
    xml_tag = "glib:boxed"

    def __init__(self, ns: "Namespace", tl: typelib.Typelib) -> None:
        super().__init__(ns, tl)

    @property
    def signature(self) -> str:
        return f"boxed {self.full_name}"

    def assignable_to(self, type) -> bool:
        return type == self

    @property
    def online_docs(self) -> T.Optional[str]:
        if ns := self.get_containing(Namespace).online_docs:
            return f"{ns}boxed.{self.name}.html"
        else:
            return None

    @property
    def deprecated(self) -> bool:
        return self.tl.STRUCT_DEPRECATED == 1


class Bitfield(Enumeration):
    xml_tag = "bitfield"

    def __init__(self, ns: "Namespace", tl: typelib.Typelib) -> None:
        super().__init__(ns, tl)


class Namespace(GirNode):
    def __init__(self, repo: "Repository", tl: typelib.Typelib) -> None:
        super().__init__(repo, tl)

    @cached_property
    def entries(self) -> T.Mapping[str, GirType]:
        entries: dict[str, GirType] = {}

        n_local_entries: int = self.tl.HEADER_N_ENTRIES
        directory: typelib.Typelib = self.tl.HEADER_DIRECTORY
        blob_size: int = self.tl.header.HEADER_ENTRY_BLOB_SIZE

        for i in range(n_local_entries):
            entry = directory[i * blob_size]
            entry_name: str = entry.DIR_ENTRY_NAME
            entry_type: int = entry.DIR_ENTRY_BLOB_TYPE
            entry_blob: typelib.Typelib = entry.DIR_ENTRY_OFFSET

            if entry_type == typelib.BLOB_TYPE_ENUM:
                entries[entry_name] = Enumeration(self, entry_blob)
            elif entry_type == typelib.BLOB_TYPE_FLAGS:
                entries[entry_name] = Bitfield(self, entry_blob)
            elif entry_type == typelib.BLOB_TYPE_OBJECT:
                entries[entry_name] = Class(self, entry_blob)
            elif entry_type == typelib.BLOB_TYPE_INTERFACE:
                entries[entry_name] = Interface(self, entry_blob)
            elif (
                entry_type == typelib.BLOB_TYPE_BOXED
                or entry_type == typelib.BLOB_TYPE_STRUCT
            ):
                entries[entry_name] = Boxed(self, entry_blob)

        return entries

    @cached_property
    def xml(self):
        return get_xml(self.name, self.version).get_elements("namespace")[0]

    @cached_property
    def name(self) -> str:
        return self.tl.HEADER_NAMESPACE

    @cached_property
    def version(self) -> str:
        return self.tl.HEADER_NSVERSION

    @property
    def signature(self) -> str:
        return f"namespace {self.name} {self.version}"

    @cached_property
    def classes(self) -> T.Mapping[str, Class]:
        return {
            name: entry
            for name, entry in self.entries.items()
            if isinstance(entry, Class)
        }

    def get_type(self, name) -> T.Optional[GirType]:
        """Gets a type (class, interface, enum, etc.) from this namespace."""
        return self.entries.get(name)

    def get_type_by_cname(self, cname: str) -> T.Optional[GirType]:
        """Gets a type from this namespace by its C name."""
        for basic in _BASIC_TYPES.values():
            if basic.glib_type_name == cname:
                return basic()

        for item in self.entries.values():
            if (
                hasattr(item, "cname")
                and item.cname is not None
                and item.cname == cname
            ):
                return item
        return None

    def lookup_type(self, type_name: str) -> T.Optional[GirType]:
        """Looks up a type in the scope of this namespace (including in the
        namespace's dependencies)."""

        ns, name = type_name.split(".", 1)
        return self.get_containing(Repository).get_type(name, ns)

    @property
    def online_docs(self) -> T.Optional[str]:
        return ONLINE_DOCS.get(f"{self.name}-{self.version}")


class Repository(GirNode):
    def __init__(self, tl: typelib.Typelib) -> None:
        super().__init__(None, tl)

        self.namespace = Namespace(self, tl)

        if dependencies := tl[0x24].string:
            deps = [tuple(dep.split("-", 1)) for dep in dependencies.split("|")]
            try:
                self.includes = {
                    name: get_namespace(name, version) for name, version in deps
                }
            except:  # pragma: no cover
                raise CompilerBugError(f"Failed to load dependencies.")
        else:
            self.includes = {}

    def get_type(self, name: str, ns: str) -> T.Optional[GirType]:
        return self.lookup_namespace(ns).get_type(name)

    def lookup_namespace(self, ns: str):
        """Finds a namespace among this namespace's dependencies."""
        if ns == self.namespace.name:
            return self.namespace
        else:
            for include in self.includes.values():
                if namespace := include.get_containing(Repository).lookup_namespace(ns):
                    return namespace

    def _resolve_dir_entry(self, dir_entry: typelib.Typelib):
        if dir_entry.DIR_ENTRY_LOCAL:
            return self.namespace.get_type(dir_entry.DIR_ENTRY_NAME)
        else:
            ns = dir_entry.DIR_ENTRY_NAMESPACE
            return self.lookup_namespace(ns).get_type(dir_entry.DIR_ENTRY_NAME)

    def _resolve_type_id(self, type_id: int) -> GirType:
        if type_id & (0xFFFFFF if sys.byteorder == "little" else 0xFFFFFF00) == 0:
            type_id = ((type_id >> 27) if sys.byteorder == "little" else type_id) & 0x1F
            # simple type
            if type_id == typelib.TYPE_BOOLEAN:
                return BoolType()
            elif type_id in [typelib.TYPE_FLOAT, typelib.TYPE_DOUBLE]:
                return FloatType()
            elif type_id in [
                typelib.TYPE_INT8,
                typelib.TYPE_INT16,
                typelib.TYPE_INT32,
                typelib.TYPE_INT64,
            ]:
                return IntType()
            elif type_id in [
                typelib.TYPE_UINT8,
                typelib.TYPE_UINT16,
                typelib.TYPE_UINT32,
                typelib.TYPE_UINT64,
            ]:
                return UIntType()
            elif type_id == typelib.TYPE_UTF8:
                return StringType()
            elif type_id == typelib.TYPE_GTYPE:
                return TypeType()
            else:
                raise CompilerBugError("Unknown type ID", type_id)
        else:
            blob = self.tl.header[type_id]
            if blob.TYPE_BLOB_TAG == typelib.TYPE_INTERFACE:
                return self._resolve_dir_entry(
                    self.tl.header[type_id].TYPE_BLOB_INTERFACE
                )
            elif blob.TYPE_BLOB_TAG == typelib.TYPE_ARRAY:
                return ArrayType(self._resolve_type_id(blob.TYPE_BLOB_ARRAY_INNER))
            else:
                raise CompilerBugError(f"{blob.TYPE_BLOB_TAG}")


class GirContext:
    def __init__(self):
        self.namespaces = {}
        self.not_found_namespaces: T.Set[str] = set()

    def add_namespace(self, namespace: Namespace):
        other = self.namespaces.get(namespace.name)
        if other is not None and other.version != namespace.version:
            raise CompileError(
                f"Namespace {namespace.name}-{namespace.version} can't be imported because version {other.version} was imported earlier"
            )

        self.namespaces[namespace.name] = namespace

    def get_type_by_cname(self, name: str) -> T.Optional[GirType]:
        for ns in self.namespaces.values():
            if type := ns.get_type_by_cname(name):
                return type
        return None

    def get_type(self, name: str, ns: str) -> T.Optional[GirType]:
        if ns is None and name in _BASIC_TYPES:
            return _BASIC_TYPES[name]()

        ns = ns or "Gtk"

        if ns not in self.namespaces:
            return None

        return self.namespaces[ns].get_type(name)

    def get_class(self, name: str, ns: str) -> T.Optional[Class]:
        type = self.get_type(name, ns)
        if isinstance(type, Class):
            return type
        else:
            return None

    def validate_ns(self, ns: str) -> None:
        """Raises an exception if there is a problem looking up the given
        namespace."""

        ns = ns or "Gtk"

        if ns not in self.namespaces and ns not in self.not_found_namespaces:
            all_available = list(set(ns for ns, _version in get_available_namespaces()))

            raise CompileError(
                f"Namespace {ns} was not imported",
                did_you_mean=(ns, all_available),
            )

    def validate_type(self, name: str, ns: str) -> None:
        """Raises an exception if there is a problem looking up the given type."""

        self.validate_ns(ns)

        type = self.get_type(name, ns)

        ns = ns or "Gtk"

        if type is None:
            raise CompileError(
                f"Namespace {ns} does not contain a type called {name}",
                did_you_mean=(name, self.namespaces[ns].classes.keys()),
            )
