"""
A module defining generator interfaces.
"""

# built-in
from contextlib import ExitStack, contextmanager
from json import dumps
from pathlib import Path
from typing import (
    Any,
    Callable,
    Dict,
    Iterable,
    Iterator,
    NamedTuple,
    Optional,
)

# third-party
from vcorelib.io import IndentedFileWriter
from vcorelib.namespace import CPP_DELIM

# internal
from ifgen import PKG_NAME, VERSION
from ifgen.environment import Generator, IfgenEnvironment

InstanceConfig = Dict[str, Any]
IfgenConfig = Dict[str, Any]


class TypeLookup(NamedTuple):
    """A container for type-lookup results."""

    name: str
    final: str
    generator: Generator


class GenerateTask(NamedTuple):
    """A container for instance-generation tasks."""

    name: str
    generator: Generator

    path: Path
    test_path: Path

    instance: InstanceConfig
    env: IfgenEnvironment

    @property
    def config(self) -> IfgenConfig:
        """Get the environment's configuration data."""
        return self.env.config.data

    def namespace(self) -> str:
        """Get this task's namespace."""

        nspace = self.env.root_namespace
        with nspace.pushed(*self.instance.get("namespace", [])):
            result = nspace.namespace(track=False)

        assert result, f"No namespace for '{self.name}'!"
        return result

    def check_custom_type(self, name: str) -> Optional[TypeLookup]:
        """Check if a name refers to a custom type."""

        final = name.split(CPP_DELIM)[-1]

        result = None
        for gen in Generator:
            if final in self.config.get(gen.value, {}):
                result = TypeLookup(name, final, gen)
                break
        return result

    def custom_include(self, name: str) -> Optional[Path]:
        """Attempt to build a path to a custom include."""

        result = None

        lookup = self.check_custom_type(name)
        if lookup is not None:
            result = (
                Path(
                    "..",
                    self.env.make_path(lookup.final, lookup.generator),
                )
                if lookup.generator != self.generator
                else Path(f"{lookup.final}.h")
            )

        return result

    def command(self, command: str, data: str = "", space: str = " ") -> str:
        """Get a doxygen command string."""
        return (
            str(self.config["command"])
            + command
            + (space if data else "")
            + data
        )

    @contextmanager
    def boilerplate(
        self,
        includes: Iterable[str] = None,
        is_test: bool = False,
        use_namespace: bool = True,
        description: str = None,
        json: bool = False,
    ) -> Iterator[IndentedFileWriter]:
        """
        Create standard generation boilerplate and yield the file writer to
        use for writing the remaining content.
        """

        with ExitStack() as stack:
            writer = stack.enter_context(
                IndentedFileWriter.from_path(
                    self.path if not is_test else self.test_path, per_indent=4
                )
            )

            # Write file header.
            with writer.javadoc():
                writer.write(self.command("file"))
                writer.write(
                    self.command(
                        "brief", f"Generated by {PKG_NAME} ({VERSION})."
                    )
                )

                if json:
                    writer.write(dumps(self.instance, indent=2))

            if not is_test:
                writer.write("#pragma once")

            with writer.padding():
                # Write any includes.
                for include in sorted(includes if includes else []):
                    if include:
                        writer.write(f"#include {include}")

            if use_namespace:
                # Write namespace.
                namespace = self.namespace()
                writer.write(f"namespace {namespace}")

                stack.enter_context(
                    writer.scope(
                        suffix=f"; // namespace {namespace}", indent=0
                    )
                )

                stack.enter_context(writer.padding())

            if description is None:
                if (
                    "description" in self.instance
                    and self.instance["description"]
                ):
                    description = self.instance["description"]

            # Write struct definition.
            if description is not None:
                with writer.javadoc():
                    writer.write(description)

            yield writer


InstanceGenerator = Callable[[GenerateTask], None]
