import re
import os.path
from html import escape


# CSS for hrepr
styledir = f"{os.path.dirname(__file__)}/style"
css_nbreset = open(f"{styledir}/nbreset.css").read()
css_hrepr = open(f"{styledir}/hrepr.css").read()


# These tags are self-closing and cannot have children.
_void_tags = {
    "area",
    "base",
    "br",
    "col",
    "command",
    "embed",
    "hr",
    "img",
    "input",
    "keygen",
    "link",
    "meta",
    "param",
    "source",
    "track",
    "wbr",
}


# We do not escape string children for these tags.
_raw_tags = {"raw", "script", "style"}


# These tags do not correspond to real HTML tags. They cannot have
# attributes, and their children are simply concatenated and inlined
# in the parent.
_virtual_tags = {None, "inline", "raw"}


class Tag:
    """
    Representation of an HTML tag.

    .. note::

        Create tags using an instance of the HTML class.
    
    A Tag should be immutable, but you can create modified versions
    of a tag by calling it with additional attributes or children,
    or indexing it with class names:

    >>> H = HTML()
    >>> tag = H.div()
    >>> tag
    <div></div>
    >>> tag(1, 2, 3)
    <div>123</div>
    >>> tag['klass']
    <div class="klass"></div>
    >>> tag(id='dividi')
    <div id="dividi"></div>
    >>> tag['klass'](id='dividi')(1, 2, 3)
    <div class="klass" id="dividi">123</div>

    Attributes:
        name (str): The tag name (div, span, a, etc.)
        attributes (dict): A dictionary of attributes. Attributes that
            start with ``__`` are reserved for internal use.
        children (tuple): Children of this node.
        resources (set): Set of resources needed by this node or its
            children.
    """

    def __init__(self, name, attributes=None, children=None):
        self.name = name
        self.attributes = attributes or {}
        self.children = children or ()
        self.resources = set()

    def fill(self, children=(), attributes={}):
        return Tag(
            self.name,
            {**self.attributes, **attributes},
            self.children + tuple(children),
        )

    def __getitem__(self, items):
        if not isinstance(items, tuple):
            items = (items,)
        classes = self.attributes.get("class", frozenset()) | frozenset(items)
        return self.fill(attributes={"class": classes})

    def __call__(self, *children, **attributes):
        if len(children) > 0 and isinstance(children[0], dict):
            attributes = {**children[0], **attributes}
            children = children[1:]
        return self.fill(children, attributes)

    def __eq__(self, other):
        return (
            isinstance(other, Tag)
            and self.name == other.name
            and self.attributes == other.attributes
            and self.children == other.children
        )

    def __hash__(self):
        return (
            hash(self.name)
            ^ hash(tuple(self.attributes.items()))
            ^ hash(self.children)
        )

    def __repr__(self):
        return str(self)

    def __str__(self):
        escape_children = not self.attributes.get(
            "__raw", self.name in _raw_tags
        )

        def convert_attribute(k, v):
            if v is True:
                return k
            elif v is False:
                return ""
            elif isinstance(v, (set, frozenset)):
                res = " ".join(escape(cls) for cls in v)
            else:
                res = escape(str(v))
            return f'{k}="{res}"'

        def convert_child(c):
            if isinstance(c, (list, tuple)):
                return "".join(map(convert_child, c))
            elif isinstance(c, Tag):
                return str(c)
            elif escape_children:
                return escape(str(c))
            else:
                return str(c)

        attr = " ".join(
            convert_attribute(k, v)
            for k, v in self.attributes.items()
            if not k.startswith("__")
        )
        if attr:
            # raw tag cannot have attributes, because it's not a real tag
            assert self.name not in _virtual_tags
            attr = " " + attr

        children = "".join(map(convert_child, self.children))
        if self.name in _virtual_tags:
            # Virtual tags just inlines their contents directly.
            return children
        if self.attributes.get("__void", self.name in _void_tags):
            assert len(self.children) == 0
            return f"<{self.name}{attr} />"
        else:
            return f"<{self.name}{attr}>{children}</{self.name}>"

    def _repr_html_(self):
        """
        Jupyter Notebook hook to print this element as HTML.
        """
        nbreset = f"<style>{css_nbreset}</style>"
        resources = "".join(map(str, self.resources))
        return f'<div>{nbreset}{resources}<div class="hrepr">{self}</div></div>'

    def as_page(self):
        """
        Wrap this Tag as a self-contained webpage. Create a page with
        the following structure:

        .. code-block:: html

            <!DOCTYPE html>
            <html>
              <head>
                <meta http-equiv="Content-type"
                      content="text/html"
                      charset="UTF-8" />
                {self.resources}
              </head>
              <body>
                {self}
              </body>
            </html>
        """
        H = HTML()
        utf8 = H.meta(
            {"http-equiv": "Content-type"}, content="text/html", charset="UTF-8"
        )
        return H.inline(
            H.raw("<!DOCTYPE html>"),
            H.html(H.head(utf8, *self.resources), H.body(self)),
        )


class HTML:
    """
    Tag factory:

    >>> H = HTML()
    >>> H.div()
    <div></div>
    >>> H.a(href="http://cool.cool.cool")("My cool site")
    <a href="http://cool.cool.cool">My cool site</a>
    >>> H.span["cool", "beans"]("How stylish!")
    <span class="cool beans">How stylish!</span>
    """

    def __getattr__(self, tag_name):
        tag_name = re.sub(
            "(?<=[a-z])([A-Z])", lambda m: f"-{m.group(0).lower()}", tag_name
        )
        return Tag(tag_name)
