"""The Mimeo Generator module.

It exports only one class:
    * Generator
        An abstract class for data generators in Mimeo.
"""
from __future__ import annotations

import logging
import xml.etree.ElementTree as ElemTree
from abc import ABCMeta, abstractmethod
from typing import Any, Iterator

from mimeo.config.mimeo_config import MimeoTemplate
from mimeo.context import MimeoContext, MimeoContextManager
from mimeo.context.decorators import (mimeo_clear_iterations, mimeo_context,
                                      mimeo_context_switch,
                                      mimeo_next_iteration)
from mimeo.utils import MimeoRenderer

logger = logging.getLogger(__name__)


class Generator(metaclass=ABCMeta):
    """An abstract class for data generators in Mimeo.

    Its subclasses are meant to be used in the Mimeo processing.
    Every supported output format has a Generator representation.

    Methods
    -------
    generate(
        templates: list | Iterator[MimeoTemplate],
        parent: Any = None
    ) -> Iterator[Any]
        Generate data based on the Mimeo Configuration.
    stringify(
        data_unit: Any
    ) -> str
        Stringify data generated by the generate() method.
    """

    @classmethod
    def __subclasshook__(
            cls,
            subclass: Generator,
    ):
        """Verify if a subclass implements all abstract methods.

        Parameters
        ----------
        subclass : Generator
            A Generator subclass

        Returns
        -------
        bool
            True if the subclass includes the generate and stringify
            methods
        """
        return ("generate" in subclass.__dict__ and callable(subclass.generate) and
                "stringify" in subclass.__dict__ and callable(subclass.stringify))

    @abstractmethod
    def generate(
            self,
            templates: list | Iterator[MimeoTemplate],
            parent: Any = None,
    ) -> Iterator[ElemTree.Element | dict]:
        """Generate data based on the Mimeo Configuration.

        It is an abstract method to implement in subclasses

        Parameters
        ----------
        templates : list | Iterator[MimeoTemplate]
            A collection of Mimeo Templates to process
        parent : Any, default None
            A parent node for the currently processed template.
            It is passed only when a Mimeo Config contain nested
            templates.

        Returns
        -------
        Iterator[ElemTree.Element | dict]
            Iterator for generated nodes
        """
        raise NotImplementedError

    @abstractmethod
    def stringify(
            self,
            data_unit: Any,
    ) -> str:
        """Stringify data generated by the generate() method.

        It is an abstract method to implement in subclasses

        Parameters
        ----------
        data_unit: Any
            A single data unit generated by the generate() method

        Returns
        -------
        str
            Stringified data unit
        """
        raise NotImplementedError

    @classmethod
    @abstractmethod
    def _pre_process_node(
            cls,
            node_meta: dict,
    ) -> dict:
        """Pre-process node's metadata.

        This function adjusts existing node's metadata and completes it with custom
        properties.

        Parameters
        ----------
        node_meta : dict
            Initial node's metadata

        Returns
        -------
        dict
            Complete node's metadata
        """
        raise NotImplementedError

    @classmethod
    @abstractmethod
    def _process_complex_value(
            cls,
            parent: ElemTree.Element | dict | list | None,
            node_meta: dict,
    ) -> ElemTree.Element | dict | list | None:
        """Process a node with a complex value.

        The node is processed accordingly to its value type.

        Parameters
        ----------
        parent : ElemTree.Element | dict | list | None
            A parent node
        node_meta : dict
            Node's metadata

        Returns
        -------
        ElemTree.Element | dict | list | None
            A processed node
        """
        raise NotImplementedError

    @classmethod
    @abstractmethod
    def _process_atomic_value(
            cls,
            parent: ElemTree.Element | dict | list | None,
            node_meta: dict,
    ) -> ElemTree.Element | dict | list | None:
        """Process a node with an atomic value.

        A parametrized Mimeo Util is considered as an atomic value as representing one.
        It renders a value for the node.

        Parameters
        ----------
        parent : ElemTree.Element | dict | list | None
            A parent node
        node_meta : dict
            Node's metadata

        Returns
        -------
        ElemTree.Element | dict | list | None
            A processed node
        """
        raise NotImplementedError

    @classmethod
    @mimeo_context_switch
    @mimeo_clear_iterations
    def _process_single_template(
            cls,
            template: MimeoTemplate,
            parent: ElemTree.Element | dict | list | None = None,
    ) -> list[ElemTree.Element | dict]:
        """Process a single Mimeo Template.

        This function is used recursively when a Mimeo Configuration
        contains nested templates.
        It repeats same processing operation so many times as it is
        configured in the `count` property of a Mimeo Configuration.
        Before the template execution it switches context managed by
        MimeoContextManager and also clears iterations as nested
        templates would collect iterations from previous parent's
        iterations.

        Parameters
        ----------
        template : MimeoTemplate
            A single Mimeo Template to process
        parent : ElemTree.Element | dict | list | None, default None
            A parent node for processing nested templates

        Returns
        -------
        list[ElemTree.Element | dict]
            A list of generated data units
        """
        logger.debug("Reading template [%s]", template)
        return [cls._process_single_data_unit(template, parent)
                for _ in iter(range(template.count))]

    @classmethod
    @mimeo_next_iteration
    def _process_single_data_unit(
            cls,
            template: MimeoTemplate,
            parent: ElemTree.Element | dict | list | None = None,
    ) -> ElemTree.Element | dict:
        """Process a single data unit from the template.

        This function is used recursively when a Mimeo Configuration
        contains nested templates.
        It processes a single data unit. The reason why it is separated
        from the _process_node() function is the @mimeo_next_iteration
        decorator. The _process_node() function is recursively called
        by itself, and it produces data for a single iteration.
        The purpose of this function is to increment iteration before
        processing node.

        Parameters
        ----------
        template : MimeoTemplate
            A single Mimeo Template to process
        parent : ElemTree.Element | dict | list | None, default None
            A parent node for processing nested templates

        Returns
        -------
        ElemTree.Element | dict
            A single data unit generated within a single template
            iteration.
        """
        node_meta = cls._node_meta(
            template.model.root_name,
            template.model.root_data)
        return cls._process_node(parent, node_meta)

    @classmethod
    def _process_node(
            cls,
            parent: ElemTree.Element | dict | list | None,
            node_meta: dict,
    ) -> ElemTree.Element | dict:
        """Process a single template's node.

        This is a recursive function that traverses Mimeo Template and generates nodes
        based on node's metadata. First, element is pre-processed, in meaning
        of metadata being adjusted. Then, element is processed accordingly to its value
        type.

        Parameters
        ----------
        parent : ElemTree.Element | dict | list | None
            A parent node
        node_meta : dict
            Node's metadata

        Returns
        -------
        ElemTree.Element | dict
            A single data unit generated within a single template iteration.
        """
        logger.fine("Rendering element - parent [%s], node_meta [%s]",
                    parent if not isinstance(parent, ElemTree.Element) else parent.tag,
                    node_meta)
        node_meta = cls._pre_process_node(node_meta)

        if cls._is_complex(node_meta):
            return cls._process_complex_value(parent, node_meta)
        return cls._process_atomic_value(parent, node_meta)

    @staticmethod
    def _is_complex(
            node_meta: dict,
    ) -> bool:
        """Verify if a node is complex.

        Parameters
        ----------
        node_meta : dict
            Node's metadata

        Returns
        -------
        bool
            True if node's value is a list or a dict not being a parametrized
            Mimeo Util. Otherwise, False.
        """
        return (isinstance(node_meta["value"], (list, dict)) and
                not node_meta["mimeo_util"])

    @staticmethod
    def _node_meta(
            name: str | None,
            value: dict | list | str | int | float | bool,
            attrs: dict | None = None,
            is_mimeo_util: bool | None = None,
            is_special_field: bool | None = None,
    ) -> dict:
        """Build node's metadata.

        Parameters
        ----------
        name : str | None
            A node's name (tag / property)
        value : dict | list | str | int | float | bool
            An node's value
        attrs : dict | None, default None
            An node's attributes
        is_mimeo_util : bool | None, default None
            A is-mimeo-util flag
        is_special_field : bool | None, default None
            A is-special-field flag

        Returns
        -------
        dict
            Entry's metadata
        """
        return {
            "name": name,
            "value": value,
            "attrs": attrs,
            "mimeo_util": is_mimeo_util,
            "special": is_special_field,
        }

    @staticmethod
    @mimeo_context
    def _render_atomic_value(
            node_meta: dict,
            context: MimeoContext | None = None,
    ) -> Any:
        """Render an atomic value for a node.

        It implements a common logic for all generators. Once the atomic value is
        rendered it is cached in references (if any configured) and special fields.

        Parameters
        ----------
        node_meta : dict
            Node's metadata
        context
            The current Mimeo Context (injected by MimeoContextManager)

        Returns
        -------
        value : Any
            A rendered value
        """
        value = MimeoRenderer.render(node_meta["value"])
        MimeoContextManager().cache_ref(node_meta["name"], value)
        if node_meta["special"]:
            context.curr_iteration().add_special_field(node_meta["name"], value)
        return value
