""" Type definitions for XSD processing.
"""

from typing import (
    Any,
    cast,
    Dict,
    List,
    Optional,
)

from anytree import (
    findall,
    NodeMixin,
    RenderTree,
    Resolver,
    ResolverError,
)
from lsprotocol.types import (
    MarkupContent,
    MarkupKind,
)
from lxml import etree

from .constants import MSG_NO_DOCUMENTATION_AVAILABLE


class XsdBase:
    """Base class that encapsulates an element from the XSD schema.

    It contains common information and functionality shared by
    XML nodes and attributes.
    """

    def __init__(self, name: str, element: Optional[etree._Element]):
        super(XsdBase, self).__init__()
        self.name: str = name
        self.xsd_element: Optional[etree._Element] = element
        self.xsd_type: Optional[etree._Element] = None

    def __repr__(self) -> str:
        return self.name

    def get_doc(self, lang: str = "en") -> MarkupContent:
        """Gets the Markdown documentation associated with this element
        from the XSD schema.

        If there is no documentation in the schema for the element,
        a message indicating this will be returned instead.

        Args:
            lang (str, optional): The language code of the documentation
            to retrieve. Defaults to "en" (English).

        Returns:
            [str]: The documentation text or a message indicating
            there is no documentation.
        """
        doc = self._get_doc_text_of_element(self.xsd_element, lang) or self._get_doc_text_of_element(self.xsd_type, lang)
        if doc:
            return MarkupContent(kind=MarkupKind.Markdown, value=doc)
        return MarkupContent(kind=MarkupKind.Markdown, value=MSG_NO_DOCUMENTATION_AVAILABLE)

    def _get_doc_text_of_element(self, element: Optional[Any], lang: str = "en") -> str:
        try:
            if element is not None:
                doc_annotation = element.xpath(
                    "./xs:annotation/xs:documentation[@xml:lang=$lang]/text()",
                    namespaces=element.nsmap,
                    lang=lang,
                )
                return cast(List[str], doc_annotation)[0].strip()
        except BaseException:
            pass
        return ""


class XsdAttribute(XsdBase):
    """Represents an attribute in an XML tag.

    It contains information about the attribute extracted
    from the XSD schema.

    Args:
        XsdBase: Inherits base functionality from XsdBase.
    """

    def __init__(
        self,
        name: str,
        element: Optional[etree._Element],
        type_name: Optional[str] = None,
        is_required: bool = False,
    ):
        super(XsdAttribute, self).__init__(name, element)
        self.type_name: Optional[str] = type_name
        self.is_required: bool = is_required
        self.enumeration: List[str] = []


class XsdNode(XsdBase, NodeMixin):
    """Represents a particular XML tag.

    The node contains information extracted from the XSD schema
    about the XML tag, possible descendants, attributes, etc.

    Args:
        XsdBase: Inherits base functionality from XsdBase.
        NodeMixin: Inherits tree node functionality from NodeMixin.
    """

    def __init__(self, name: str, element: Optional[etree._Element] = None, parent: Optional[NodeMixin] = None):
        super(XsdNode, self).__init__(name, element)
        self.parent: Optional[NodeMixin] = parent
        self.attributes: Dict[str, XsdAttribute] = {}
        self.min_occurs: int = 1  # required by default
        self.max_occurs: int = -1  # unbounded by default

    def render(self) -> str:
        """Gets an ascii representation of this node.

        Returns:
            str: An ascii representation of the node.
        """
        return str(RenderTree(self).by_attr(lambda node: f"[{node.name}] {' '.join(node.attributes)}"))


class XsdTree:
    """Represents a tree structure containing the important
    XSD information for all the elements and attributes.
    """

    def __init__(self, root: XsdNode):
        self.root: XsdNode = root
        self.node_resolver = Resolver("name")
        self.expand_element = self._build_expand_element()

    def find_node_by_stack(self, node_stack: List[str]) -> Optional[XsdNode]:
        """Finds the node definition in the tree that matches the given stack of tags.

        Args:
            node_stack (List[str]): The stack of tag names composing a tree branch.
            Like: ['root', 'node', 'subnode', 'leaf']

        Returns:
            Optional[XsdNode]: The node definition matching the leaf in the path or
            None if the node could not be found.
        """
        result_node = None
        if node_stack:
            try:
                path = self._get_path_from_stack(node_stack)
                if path.endswith(self.expand_element.name):
                    return self.expand_element
                result_node = self.node_resolver.get(self.root, path)
            except ResolverError as e:
                print(e)
        return result_node

    def render(self) -> str:
        """Gets an ascii representation of this tree.

        Returns:
            str: An ascii representation of the tree.
        """
        return self.root.render()

    def _get_path_from_stack(self, node_stack: List[str]) -> str:
        if node_stack[0] == self.root.name:
            node_stack[0] = "."
        return "/".join(node_stack)

    def _build_expand_element(self) -> XsdNode:
        """Creates a XsdNode representing the <expand> element and it's attributes.
        The <expand> element is a special element that is not defined in the XSD schema but allows to use macros in the tool.

        Returns:
            XsdNode: The node with information about the special <expand> element.
        """
        expand_node = XsdNode("expand", None)
        attr_name = "macro"
        attr = XsdAttribute(attr_name, None, type_name=None, is_required=True)
        expand_node.attributes[attr_name] = attr
        return expand_node

    def find_node_by_name(self, name: str) -> Optional[XsdNode]:
        if name == self.expand_element.name:
            return self.expand_element

        result = findall(
            self.root,
            filter_=lambda node: node.name == name,
        )
        if len(result) > 0:
            return cast(XsdNode, result[0])
        return None
