"""The Mimeo XML Generator module.

It exports only one class:
    * XMLGenerator
        A Generator implementation producing data in the XML format.
"""
from __future__ import annotations

import logging
import xml.etree.ElementTree as ElemTree
from typing import Iterator
from xml.dom import minidom

from mimeo.config import constants as cc
from mimeo.config.mimeo_config import MimeoConfig, MimeoTemplate
from mimeo.generators import Generator
from mimeo.generators.exc import UnsupportedStructureError
from mimeo.utils import MimeoRenderer

logger = logging.getLogger(__name__)


class XMLGenerator(Generator):
    """A Generator implementation producing data in the XML format.

    This Generator is instantiated for the 'xml' output format
    and produces data using Mimeo Configuration.

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

    def __init__(
            self,
            mimeo_config: MimeoConfig,
    ):
        """Initialize XMLGenerator class.

        Parameters
        ----------
        mimeo_config : MimeoConfig
            A Mimeo Configuration
        """
        self._indent: int = mimeo_config.output.indent
        self._xml_declaration: bool = mimeo_config.output.xml_declaration

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

        This function is used recursively when a Mimeo Configuration
        contains nested templates.
        It iterates through all templates configured and yields data
        units.

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

        Returns
        -------
        Iterator[ElemTree.Element]
            Iterator for generated nodes
        """
        for template in templates:
            for data_unit in cls._process_single_template(template, parent):
                yield data_unit

    def stringify(
            self,
            data_unit: ElemTree.Element,
    ) -> str:
        """Stringify XML data generated by the generate() method.

        Parameters
        ----------
        data_unit: ElemTree.Element
            A single data unit generated by the generate() method

        Returns
        -------
        str
            Stringified data unit
        """
        if self._indent is None or self._indent == 0:
            node_str = ElemTree.tostring(
                data_unit,
                encoding="utf-8",
                method="xml",
                xml_declaration=self._xml_declaration)
        else:
            xml_string = ElemTree.tostring(data_unit)
            xml_minidom = minidom.parseString(xml_string)
            if self._xml_declaration is False:
                xml_minidom = xml_minidom.childNodes[0]
            node_str = xml_minidom.toprettyxml(
                indent=" " * self._indent,
                encoding="utf-8")
        return node_str.decode("ascii")

    @classmethod
    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:
        * the name property is changed only for special fields
          - field name is extracted
        * the value property is being modified for dicts including attributes
          - properties starting with '@' are being removed from the dict
        * the attrs property
          - is being populated by all properties starting with '@'
          - takes the default value (an empty dict) when there's no such properties
        * the mimeo_util property is being initialized
          - True if element's value is a parametrized mimeo_util. Otherwise, False.
        * the special property is being initialized
          - True, when a field is special. Otherwise, False.

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

        Returns
        -------
        dict
            Complete node's metadata
        """
        name = node_meta["name"]
        value = node_meta["value"]
        attrs = node_meta["attrs"] if node_meta["attrs"] is not None else {}
        is_mimeo_util = MimeoRenderer.is_parametrized_mimeo_util(value)
        is_special_field = MimeoRenderer.is_special_field(name)
        if is_special_field:
            name = MimeoRenderer.get_special_field_name(name)
        if isinstance(value, dict):
            value_copy = dict(value)
            for prop in value:
                if prop.startswith(cc.MODEL_ATTRIBUTES_KEY):
                    attrs[prop[1:]] = value_copy.pop(prop)
            value = value.get(cc.MODEL_TEXT_VALUE_KEY, value_copy)

        return cls._node_meta(
            name,
            value,
            attrs,
            is_mimeo_util,
            is_special_field)

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

        The node is processed accordingly to its value type.
        When the type is list and name is _templates_, node is processed
        in a special way.

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

        Returns
        -------
        ElemTree.Element
            A processed node

        Raises
        ------
        UnsupportedStructureError
            If a list value elements are not atomic-only or dict-only.
        InvalidSpecialFieldValueError
            If a special field value is dict or list
        SpecialFieldNotFoundError
            If a special field does not exist.
        """
        if isinstance(node_meta["value"], dict):
            func = cls._process_dict_value
        elif (isinstance(node_meta["value"], list) and
              node_meta["name"] != cc.TEMPLATES_KEY):
            func = cls._process_list_value
        else:
            func = cls._process_templates_value
        return func(parent, node_meta)

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

        It iterates through the dictionary items and processes each of them.

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

        Returns
        -------
        ElemTree.Element
            A processed node

        Raises
        ------
        UnsupportedStructureError
            If a list value elements are not atomic-only or dict-only.
        InvalidSpecialFieldValueError
            If a special field value is dict or list
        SpecialFieldNotFoundError
            If a special field does not exist.

        Examples
        --------
        parent = ElemTree.Element("Root")
        node_meta = cls._node_meta(
            name="SomeField",
            value={"SomeChild1": 1, "SomeChild2": 2},
        )
        cls._process_dict_value(parent, node_meta)
        ->
        <SomeField>
            <SomeChild1>1</SomeChild1>
            <SomeChild2>2</SomeChild2>
        </SomeField>
        """
        element = cls._create_node(parent, node_meta)
        for child_tag, child_value in node_meta["value"].items():
            cls._process_node(element, cls._node_meta(child_tag, child_value))
        return element

    @classmethod
    def _process_list_value(
            cls,
            parent: ElemTree.Element,
            node_meta: dict,
    ) -> ElemTree.Element:
        """Process a node with a list value.

        It iterates through the list items and processes each of them: generates
        a child element for each value as direct children of the parent.

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

        Returns
        -------
        ElemTree.Element
            A processed node

        Raises
        ------
        UnsupportedStructureError
            If any of the list value element is a list.
        InvalidSpecialFieldValueError
            If the special field value is dict or list
        SpecialFieldNotFoundError
            If the special field does not exist.

        Examples
        --------
        parent = ElemTree.Element("Root")
        node_meta = cls._node_meta(
            name="SomeField",
            value=[
                'value-1',
                {'SomeChild1': True, 'SomeChild2': False},
                {'_mimeo_util': {'_name': 'auto_increment', 'pattern': '{}'}}
            ],
        )
        cls._process_list_value_with_atomic_children(parent, node_meta)
        ->
        <Root>
            <SomeField>value-1</SomeField>
            <SomeField>
                <SomeChild1>true</SomeChild1>
                <SomeChild2>false</SomeChild2>
            </SomeField>
            <SomeField>1</SomeField>
        </Root>
        """
        for child in node_meta["value"]:
            if isinstance(child, list):
                raise UnsupportedStructureError(
                    node_meta["name"],
                    node_meta["value"])
            node_meta = cls._node_meta(
                name=node_meta["name"],
                value=child,
                attrs=node_meta["attrs"])
            cls._process_node(parent, node_meta)
        return parent

    @classmethod
    def _process_templates_value(
            cls,
            parent: ElemTree.Element,
            node_meta: dict,
    ) -> ElemTree.Element:
        """Process a node with a dictionary value storing templates.

        It iterates through the templates and generates data based on them.

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

        Returns
        -------
        ElemTree.Element
            A processed node

        Raises
        ------
        UnsupportedStructureError
            If a list value elements are not atomic-only or dict-only.
        InvalidSpecialFieldValueError
            If a special field value is dict or list
        SpecialFieldNotFoundError
            If a special field does not exist.

        Examples
        --------
        parent = ElemTree.Element("Root")
        node_meta = cls._node_meta(
            name="SomeField",
            value={"_templates_": [
                {
                  "count": 10,
                  "model": {
                    "SomeChild": {
                      "Node1": 1,
                      "Node2": "value-2",
                      "Node3": true
                    }
                  }
                }
            ]},
        )
        cls._process_templates_value(parent, node_meta)
        ->
        <Root>
            <SomeField>
                <SomeChild><Node1>1</Node1><Node2>value-2</Node2><Node3>true</Node3></SomeChild>
                <SomeChild><Node1>1</Node1><Node2>value-2</Node2><Node3>true</Node3></SomeChild>
                ... x10
            </SomeField>
        </Root>
        """
        templates = (MimeoTemplate(template) for template in node_meta["value"])
        for _ in cls.generate(templates, parent):
            pass
        return parent

    @classmethod
    def _process_atomic_value(
            cls,
            parent: ElemTree.Element,
            node_meta: dict,
    ) -> ElemTree.Element:
        """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 | None
            A parent node
        node_meta : dict
            Node's metadata

        Returns
        -------
        ElemTree.Element
            A processed node

        Raises
        ------
        UnsupportedStructureError
            If a list value elements are not atomic-only or dict-only.
        InvalidSpecialFieldValueError
            If a special field value is dict or list
        SpecialFieldNotFoundError
            If a special field does not exist.

        Examples
        --------
        parent = ElemTree.Element("Root")
        node_meta = cls._node_meta(
            name="SomeField",
            value="value-1",
        )
        cls._process_atomic_value(parent, node_meta)
        ->
        <SomeField>value-1</SomeField>
        """
        element = cls._create_node(parent, node_meta)
        value = super()._render_atomic_value(node_meta)
        val_str = str(value) if value is not None else ""
        element.text = val_str.lower() if isinstance(value, bool) else val_str
        logger.fine("Rendered value [%s]", element.text)
        return element

    @staticmethod
    def _create_node(
            parent: ElemTree.Element,
            node_meta: dict,
    ) -> ElemTree.Element | ElemTree.SubElement:
        """Create an XML node based on the `parent` existence.

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

        Returns
        -------
        ElemTree.Element | ElemTree.SubElement
            If the `parent` is None, returns ElemTree.Element.
            Otherwise, returns ElemTree.SubElement
        """
        if parent is None:
            return ElemTree.Element(
                node_meta["name"],
                attrib=node_meta["attrs"])
        return ElemTree.SubElement(
            parent,
            node_meta["name"],
            attrib=node_meta["attrs"],
        )
