import re
from typing import Any, Optional

from draftjs_exporter.engines.base import DOMEngine
from draftjs_exporter.types import HTML, Element, Props, RenderableType
from draftjs_exporter.utils.module_loading import import_string

# https://gist.github.com/yahyaKacem/8170675
_first_cap_re = re.compile(r"(.)([A-Z][a-z]+)")
_all_cap_re = re.compile("([a-z0-9])([A-Z])")


class DOM:
    """
    Component building API, abstracting the DOM implementation.
    """

    HTML5LIB = "draftjs_exporter.engines.html5lib.DOM_HTML5LIB"
    LXML = "draftjs_exporter.engines.lxml.DOM_LXML"
    STRING = "draftjs_exporter.engines.string.DOMString"
    STRING_COMPAT = "draftjs_exporter.engines.string_compat.DOMStringCompat"

    dom: DOMEngine = None  # type: ignore

    @staticmethod
    def camel_to_dash(camel_cased_str: str) -> str:
        sub2 = _first_cap_re.sub(r"\1-\2", camel_cased_str)
        dashed_case_str = _all_cap_re.sub(r"\1-\2", sub2).lower()
        return dashed_case_str.replace("--", "-")

    @classmethod
    def use(cls, engine: str) -> None:
        """
        Choose which DOM implementation to use.
        """
        cls.dom = import_string(engine)

    @classmethod
    def create_element(
        cls,
        type_: RenderableType = None,
        props: Optional[Props] = None,
        *elt_children: Optional[Element],
    ) -> Element:
        """
        Signature inspired by React.createElement.
        createElement(
          string/Component type,
          [dict props],
          [children ...]
        )
        https://facebook.github.io/react/docs/top-level-api.html#react.createelement
        """
        # Create an empty document fragment.
        if not type_:
            return cls.dom.create_tag("fragment")

        if props is None:
            props = {}

        # If the first element of children is a list, we use it as the list.
        if len(elt_children) and isinstance(elt_children[0], (list, tuple)):
            children = elt_children[0]
        else:
            children = elt_children

        # The children prop is the first child if there is only one.
        props["children"] = children[0] if len(children) == 1 else children

        if callable(type_):
            # Function component, via def or lambda.
            elt = type_(props)
        else:
            # Raw tag, as a string.
            attributes = {}

            # Never render those attributes on a raw tag.
            props.pop("children", None)
            props.pop("block", None)
            props.pop("blocks", None)
            props.pop("entity", None)
            props.pop("inline_style_range", None)

            # Convert style object to style string, like the DOM would do.
            if "style" in props and isinstance(props["style"], dict):
                rules = [
                    f"{DOM.camel_to_dash(s)}: {v};" for s, v in props["style"].items()
                ]
                props["style"] = "".join(rules)

            # Convert props to HTML attributes.
            for key in props:
                if props[key] is False:
                    props[key] = "false"

                if props[key] is True:
                    props[key] = "true"

                if props[key] is not None:
                    attributes[key] = str(props[key])

            elt = cls.dom.create_tag(type_, attributes)

            # Append the children inside the element.
            for child in children:
                if child not in (None, ""):
                    cls.append_child(elt, child)

        # If elt is "empty", create a fragment anyway to add children.
        if elt in (None, ""):
            elt = cls.dom.create_tag("fragment")

        return elt

    @classmethod
    def parse_html(cls, markup: HTML) -> Element:
        return cls.dom.parse_html(markup)

    @classmethod
    def append_child(cls, elt: Element, child: Element) -> Any:
        return cls.dom.append_child(elt, child)

    @classmethod
    def render(cls, elt: Element) -> HTML:
        return cls.dom.render(elt)

    @classmethod
    def render_debug(cls, elt: Element) -> HTML:
        return cls.dom.render_debug(elt)
