"""
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,
    Union,
)

# third-party
from runtimepy.codec.protocol import Protocol
from runtimepy.enum import RuntimeEnum
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

    def cpp_namespace(
        self, data: str, header: bool = True, prefix: str = None
    ) -> str:
        """Namespace a string with this task's name."""

        if not header:
            data = f"{self.name}::{data}"
        return data if not prefix else prefix + data

    @property
    def stream_implementation(self) -> bool:
        """
        Determine if this instances should include a stream implementations.
        """
        return self.instance.get("stream", True)  # type: ignore

    @property
    def source_path(self) -> Path:
        """Get a source file for this task."""
        return self.path.with_suffix(".cc")

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

    def enum(self) -> RuntimeEnum:
        """Look up a runtime enumeration for this task."""

        return self.env.get_enum(self.name)

    def protocol(self) -> Protocol:
        """Loop up a protocol for this task."""

        return self.env.get_protocol(self.name)

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

        nspace = self.env.types.root_namespace
        with nspace.pushed(*self.instance.get("namespace", []), *names):
            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 source_boilerplate(
        self, includes: Iterable[str]
    ) -> Iterator[IndentedFileWriter]:
        """Create standard generation boilerplate for a source file."""

        with ExitStack() as stack:
            writer = stack.enter_context(
                IndentedFileWriter.from_path(self.source_path, per_indent=4)
            )

            self.javadoc_header(writer)

            self.write_includes(
                writer,
                includes=[
                    f'"{self.env.rel_include(self.name, self.generator)}"'
                ]
                + list(includes),
            )

            self.handle_namespace(stack, writer)

            yield writer

    def javadoc_header(
        self, writer: IndentedFileWriter, json: bool = False
    ) -> None:
        """Write a standard 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))

    def handle_namespace(
        self, stack: ExitStack, writer: IndentedFileWriter
    ) -> None:
        """Write namespace boilerplate to a file."""

        # 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())

    def write_includes(
        self, writer: IndentedFileWriter, includes: Iterable[str] = None
    ) -> None:
        """Write sorted includes to a file."""

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

    @contextmanager
    def boilerplate(
        self,
        includes: Iterable[str] = None,
        is_test: bool = False,
        use_namespace: bool = True,
        description: Union[bool, None, 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.
            self.javadoc_header(writer, json=json)

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

            self.write_includes(writer, includes=includes)

            if use_namespace:
                self.handle_namespace(stack, writer)

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

            # Write struct definition.
            if description:
                with writer.javadoc():
                    writer.write(description)  # type: ignore

            yield writer

    def has_instances(self) -> bool:
        """Determine whether or not this instance has concrete instances."""
        return bool(self.instance.get("instances", []))

    def method_suffix(self) -> str:
        """Get a possible suffix for a method."""
        return "" if not self.has_instances() else " volatile"


InstanceGenerator = Callable[[GenerateTask], None]
