#
# This file is licensed under the MIT No Attribution license.
#

"""Inline the SVG objects produced by groff and dvisvgm into their HTML."""

import hashlib
import re
import sys
from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import unquote, urljoin, urlsplit
import xml.etree.ElementTree as ET


SVG = "{http://www.w3.org/2000/svg}"
XLINK = "{http://www.w3.org/1999/xlink}"
ET.register_namespace("", SVG[1:-1])
ET.register_namespace("xlink", XLINK[1:-1])


class Page(HTMLParser):
    def __init__(self, path):
        super().__init__(convert_charrefs=False)
        self.path = path
        self.source = path.read_bytes().decode("utf-8", errors="surrogateescape")
        self.offsets = [0]
        for line in self.source.split("\n"):
            self.offsets.append(self.offsets[-1] + len(line) + 1)
        self.objects = []
        self.pending = None
        self.ids = set()
        self.fonts = set()
        self.images = set()

    def position(self):
        line, column = self.getpos()
        return self.offsets[line - 1] + column

    def handle_starttag(self, tag, attributes):
        attributes = dict(attributes)
        if "id" in attributes:
            self.ids.add(attributes["id"])
        if tag == "object" and attributes.get("type") == "image/svg+xml":
            self.pending = (self.position(), attributes)

    def handle_endtag(self, tag):
        if tag == "object" and self.pending is not None:
            start, attributes = self.pending
            end = self.source.index(">", self.position()) + 1
            self.objects.append((start, end, attributes))
            self.pending = None

    def inline(self, attributes, number):
        source = attributes["data"]
        path = self.path.parent / unquote(urlsplit(source).path)
        root = ET.parse(path).getroot()
        self.images.add(path)
        prefix = f"svg-{number}"
        while any(value.startswith(prefix + "-") or value == prefix
                  for value in self.ids):
            prefix += "-svg"
        ids = {element.get("id"): prefix + "-" + element.get("id")
               for element in root.iter() if element.get("id")}

        def href(value):
            if value.startswith("#"):
                return "#" + ids.get(value[1:], value[1:])
            return urljoin(source, value)

        def urls(value):
            return re.sub(r"url\((['\"]?)(.*?)\1\)",
                          lambda match: "url(" + match[1] + href(match[2])
                          + match[1] + ")", value)

        for element in root.iter():
            if element.get("id") in ids:
                element.set("id", ids[element.get("id")])
            for name, value in list(element.attrib.items()):
                if name in ("href", XLINK + "href"):
                    element.set(name, href(value))
                elif "url(" in value:
                    element.set(name, urls(value))
            if element.tag != SVG + "style":
                continue

            families = {}

            def font(match):
                declaration = match[1]
                family = re.search(r"font-family:([^;]+);", declaration)
                face = declaration[:family.start()] + declaration[family.end():]
                name = "svg-font-" + hashlib.sha256(face.encode()).hexdigest()
                families[family[1]] = name
                if name in self.fonts:
                    return ""
                self.fonts.add(name)
                return "@font-face{font-family:" + name + ";" + face + "}"

            style = re.sub(r"@font-face\{([^}]+)\}", font, element.text or "")

            def rule(match):
                selector, declaration = match[1].strip(), match[2]
                if selector == "@font-face":
                    return match[0]
                declaration = re.sub(
                    r"font-family:([^;]+);",
                    lambda family: "font-family:"
                    + families.get(family[1], family[1]) + ";", declaration)
                selectors = ["#" + prefix + " " + part.strip()
                             for part in selector.split(",")]
                return "\n" + ", ".join(selectors) + " {" + urls(declaration) + "}"

            element.text = re.sub(r"([^{}]+)\{([^{}]*)\}", rule, style) + "\n"

        # The enclosing object's dimensions use CSS pixels; the SVG source
        # can use points. Keep the displayed size when changing elements.
        for name in ("width", "height", "style"):
            if name in attributes:
                root.set(name, attributes[name])
        # SVG text must not inherit bold or italic from surrounding HTML.
        root.set("style", "font: initial; " + root.get("style", ""))
        root.set("id", prefix)
        root.set("class", (root.get("class", "") + " inline-svg").strip())
        self.ids.add(prefix)
        self.ids.update(ids.values())
        return ET.tostring(root, encoding="unicode")

    def write(self):
        self.feed(self.source)
        if not self.objects:
            return
        parts = []
        end = 0
        for number, (start, stop, attributes) in enumerate(self.objects, 1):
            parts.append(self.source[end:start])
            parts.append(self.inline(attributes, number))
            end = stop
        parts.append(self.source[end:])
        self.path.write_bytes("".join(parts).encode("utf-8", errors="surrogateescape"))


directory = Path(sys.argv[1])
images = set()
for path in sorted(directory.rglob("*.html")):
    if b"<object" not in path.read_bytes():
        continue
    page = Page(path)
    page.write()
    images.update(page.images)
for path in sorted(images):
    path.unlink()
