from typing import Generator, Sequence, Any

from .configuration import SfgCodeStyle
from .ir.source_components import (
    SfgHeaderInclude,
    SfgKernelNamespace,
    SfgFunction,
    SfgClass,
)
from .exceptions import SfgException


class SfgContext:
    """Represents a header/implementation file pair in the code generator.

    **Source File Properties and Components**

    The SfgContext collects all properties and components of a header/implementation
    file pair (or just the header file, if header-only generation is used).
    These are:

    - The code namespace, which is combined from the `outer_namespace`
      and the `pystencilssfg.SfgContext.inner_namespace`. The outer namespace is meant to be set
      externally e.g. by the project configuration, while the inner namespace is meant to be set by the generator
      script.
    - The `prelude comment` is a block of text printed as a comment block
      at the top of both generated files. Typically, it contains authorship and licence information.
    - The set of included header files (`pystencilssfg.SfgContext.includes`).
    - Custom `definitions`, which are just arbitrary code strings.
    - Any number of kernel namespaces (`pystencilssfg.SfgContext.kernel_namespaces`), within which *pystencils*
      kernels are managed.
    - Any number of functions (`pystencilssfg.SfgContext.functions`), which are meant to serve as wrappers
      around kernel calls.
    - Any number of classes (`pystencilssfg.SfgContext.classes`), which can be used to build more extensive wrappers
      around kernels.

    **Order of Definitions**

    To honor C/C++ use-after-declare rules, the context preserves the order in which definitions, functions and classes
    are added to it.
    The header file printers implemented in *pystencils-sfg* will print the declarations accordingly.
    The declarations can retrieved in order of definition via `declarations_ordered`.
    """

    def __init__(
        self,
        outer_namespace: str | None = None,
        codestyle: SfgCodeStyle = SfgCodeStyle(),
        argv: Sequence[str] | None = None,
        project_info: Any = None,
    ):
        """
        Args:
            outer_namespace: Qualified name of the outer code namespace
            codestyle: Code style that should be used by the code emitter
            argv: The generator script's command line arguments.
                Reserved for internal use by the [SourceFileGenerator][pystencilssfg.SourceFileGenerator].
            project_info: Project-specific information provided by a build system.
                Reserved for internal use by the [SourceFileGenerator][pystencilssfg.SourceFileGenerator].
        """
        self._argv = argv
        self._project_info = project_info
        self._default_kernel_namespace = SfgKernelNamespace(self, "kernels")

        self._outer_namespace = outer_namespace
        self._inner_namespace: str | None = None

        self._codestyle = codestyle

        #   Source Components
        self._prelude: str = ""
        self._includes: set[SfgHeaderInclude] = set()
        self._definitions: list[str] = []
        self._kernel_namespaces = {
            self._default_kernel_namespace.name: self._default_kernel_namespace
        }
        self._functions: dict[str, SfgFunction] = dict()
        self._classes: dict[str, SfgClass] = dict()

        self._declarations_ordered: list[str | SfgFunction | SfgClass] = list()

        #   Standard stuff
        self.add_include(SfgHeaderInclude("cstdint", system_header=True))
        self.add_definition("#define RESTRICT __restrict__")

    @property
    def argv(self) -> Sequence[str]:
        """If this context was created by a `pystencilssfg.SourceFileGenerator`, provides the command
        line arguments given to the generator script, with configuration arguments for the code generator
        stripped away.

        Otherwise, throws an exception."""
        if self._argv is None:
            raise SfgException("This context provides no command-line arguments.")
        return self._argv

    @property
    def project_info(self) -> Any:
        """Project-specific information provided by a build system."""
        return self._project_info

    @property
    def outer_namespace(self) -> str | None:
        """Outer code namespace. Set by constructor argument `outer_namespace`."""
        return self._outer_namespace

    @property
    def inner_namespace(self) -> str | None:
        """Inner code namespace. Set by `set_namespace`."""
        return self._inner_namespace

    @property
    def fully_qualified_namespace(self) -> str | None:
        """Combined outer and inner namespaces, as `outer_namespace::inner_namespace`."""
        match (self.outer_namespace, self.inner_namespace):
            case None, None:
                return None
            case outer, None:
                return outer
            case None, inner:
                return inner
            case outer, inner:
                return f"{outer}::{inner}"
            case _:
                assert False

    @property
    def codestyle(self) -> SfgCodeStyle:
        """The code style object for this generation context."""
        return self._codestyle

    # ----------------------------------------------------------------------------------------------
    #   Prelude, Includes, Definitions, Namespace
    # ----------------------------------------------------------------------------------------------

    @property
    def prelude_comment(self) -> str:
        """The prelude is a comment block printed at the top of both generated files."""
        return self._prelude

    def append_to_prelude(self, code_str: str):
        """Append a string to the prelude comment.

        The string should not contain
        C/C++ comment delimiters, since these will be added automatically during
        code generation.
        """
        if self._prelude:
            self._prelude += "\n"

        self._prelude += code_str

        if not code_str.endswith("\n"):
            self._prelude += "\n"

    def includes(self) -> Generator[SfgHeaderInclude, None, None]:
        """Includes of headers. Public includes are added to the header file, private includes
        are added to the implementation file."""
        yield from self._includes

    def add_include(self, include: SfgHeaderInclude):
        self._includes.add(include)

    def definitions(self) -> Generator[str, None, None]:
        """Definitions are arbitrary custom lines of code."""
        yield from self._definitions

    def add_definition(self, definition: str):
        """Add a custom code string to the header file."""
        self._definitions.append(definition)
        self._declarations_ordered.append(definition)

    def set_namespace(self, namespace: str):
        """Set the inner code namespace.

        Throws an exception if the namespace was already set.
        """
        if self._inner_namespace is not None:
            raise SfgException("The code namespace was already set.")

        self._inner_namespace = namespace

    # ----------------------------------------------------------------------------------------------
    #   Kernel Namespaces
    # ----------------------------------------------------------------------------------------------

    @property
    def default_kernel_namespace(self) -> SfgKernelNamespace:
        """The default kernel namespace."""
        return self._default_kernel_namespace

    def kernel_namespaces(self) -> Generator[SfgKernelNamespace, None, None]:
        """Iterator over all registered kernel namespaces."""
        yield from self._kernel_namespaces.values()

    def get_kernel_namespace(self, str) -> SfgKernelNamespace | None:
        """Retrieve a kernel namespace by name, or `None` if it does not exist."""
        return self._kernel_namespaces.get(str)

    def add_kernel_namespace(self, namespace: SfgKernelNamespace):
        """Adds a new kernel namespace.

        If a kernel namespace of the same name already exists, throws an exception.
        """
        if namespace.name in self._kernel_namespaces:
            raise ValueError(f"Duplicate kernel namespace: {namespace.name}")

        self._kernel_namespaces[namespace.name] = namespace

    # ----------------------------------------------------------------------------------------------
    #   Functions
    # ----------------------------------------------------------------------------------------------

    def functions(self) -> Generator[SfgFunction, None, None]:
        """Iterator over all registered functions."""
        yield from self._functions.values()

    def get_function(self, name: str) -> SfgFunction | None:
        """Retrieve a function by name. Returns `None` if no function of the given name exists."""
        return self._functions.get(name, None)

    def add_function(self, func: SfgFunction):
        """Adds a new function.

        If a function or class with the same name exists already, throws an exception.
        """
        if func.name in self._functions or func.name in self._classes:
            raise SfgException(f"Duplicate function: {func.name}")

        self._functions[func.name] = func
        self._declarations_ordered.append(func)

    # ----------------------------------------------------------------------------------------------
    #   Classes
    # ----------------------------------------------------------------------------------------------

    def classes(self) -> Generator[SfgClass, None, None]:
        """Iterator over all registered classes."""
        yield from self._classes.values()

    def get_class(self, name: str) -> SfgClass | None:
        """Retrieve a class by name, or `None` if the class does not exist."""
        return self._classes.get(name, None)

    def add_class(self, cls: SfgClass):
        """Add a class.

        Throws an exception if a class or function of the same name exists already.
        """
        if cls.class_name in self._classes or cls.class_name in self._functions:
            raise SfgException(f"Duplicate class: {cls.class_name}")

        self._classes[cls.class_name] = cls
        self._declarations_ordered.append(cls)

    # ----------------------------------------------------------------------------------------------
    #   Declarations in order of addition
    # ----------------------------------------------------------------------------------------------

    def declarations_ordered(
        self,
    ) -> Generator[str | SfgFunction | SfgClass, None, None]:
        """All declared definitions, classes and functions in the order they were added.

        Awareness about order is necessary due to the C++ declare-before-use rules."""
        yield from self._declarations_ordered