"""Inline every local asset of src/index.html into one dist/index.html.

Usage (from the playable's output folder):
    python build/inline_build.py [--src src/index.html] [--out dist/index.html]

What it inlines, relative to the source html's folder:
  <script src="..."></script>            -> <script>...</script>
  <link rel="stylesheet" href="...">     -> <style>...</style>
  src="x.png" / href="x.mp3" / url(x.ttf) in html, css and js strings -> data: URIs
Remote URLs (http/https/data) are left alone and reported, because a playable must not load
anything at runtime. Exit code 1 when the result is over the size limit (5 MB by default).
"""
import argparse
import base64
import mimetypes
import os
import re
import subprocess
import sys

ESBUILD = "esbuild@0.25.4"


def minify(path, text, kind, report):
    """Minify game JS or CSS with esbuild through npx. Already-minified files (*.min.js) are
    left alone. When esbuild cannot run (no Node, no network on first use) the file goes in
    unminified and the report says so, because a build that ships beats a build that fails."""
    if path.lower().endswith((".min.js", ".min.css")):
        return text
    try:
        r = subprocess.run(
            ["npx", "-y", ESBUILD, "--minify", "--legal-comments=none", f"--loader={kind}",
             "--log-level=error"],
            input=text, capture_output=True, text=True, encoding="utf-8",
            shell=(os.name == "nt"), timeout=120)
        if r.returncode == 0 and r.stdout.strip():
            report["minified"][path] = (len(text), len(r.stdout))
            return r.stdout
        report["unminified"][path] = (r.stderr or "esbuild returned nothing").strip()[:200]
    except (OSError, subprocess.TimeoutExpired) as e:
        report["unminified"][path] = str(e)[:200]
    return text

MIME = {
    ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp",
    ".gif": "image/gif", ".svg": "image/svg+xml", ".mp3": "audio/mpeg", ".ogg": "audio/ogg",
    ".wav": "audio/wav", ".ttf": "font/ttf", ".otf": "font/otf", ".woff": "font/woff",
    ".woff2": "font/woff2", ".json": "application/json",
}
ASSET_EXT = tuple(MIME)


def data_uri(path):
    ext = os.path.splitext(path)[1].lower()
    mime = MIME.get(ext) or mimetypes.guess_type(path)[0] or "application/octet-stream"
    with open(path, "rb") as f:
        return f"data:{mime};base64,{base64.b64encode(f.read()).decode('ascii')}"


def inline_assets(text, base, report):
    """Replace quoted or url() references to local asset files with data URIs."""
    pattern = re.compile(r"""(["'(])([^"'()\s]+?\.(?:%s))(["')])""" % "|".join(
        e.lstrip(".") for e in ASSET_EXT), re.IGNORECASE)

    def sub(m):
        ref = m.group(2)
        if ref.startswith(("http://", "https://", "data:", "//")):
            report["remote"].add(ref)
            return m.group(0)
        path = os.path.normpath(os.path.join(base, ref))
        if not os.path.isfile(path):
            report["missing"].add(ref)
            return m.group(0)
        report["inlined"][ref] = os.path.getsize(path)
        return m.group(1) + data_uri(path) + m.group(3)

    return pattern.sub(sub, text)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--src", default="src/index.html")
    ap.add_argument("--out", default="dist/index.html")
    ap.add_argument("--limit-mb", type=float, default=5.0)
    ap.add_argument("--no-minify", action="store_true", help="inline JS and CSS as written")
    a = ap.parse_args()

    base = os.path.dirname(os.path.abspath(a.src))
    with open(a.src, encoding="utf-8") as f:
        html = f.read()
    report = {"inlined": {}, "missing": set(), "remote": set(), "minified": {}, "unminified": {}}

    def script_sub(m):
        src = m.group(1)
        if src.startswith(("http", "//")):
            report["remote"].add(src)
            return m.group(0)
        path = os.path.join(base, src)
        with open(path, encoding="utf-8") as f:
            js = f.read()
        report["inlined"][src] = os.path.getsize(path)
        if not a.no_minify:
            js = minify(src, js, "js", report)
        return "<script>\n" + inline_assets(js, os.path.dirname(path), report) + "\n</script>"

    def link_sub(m):
        href = m.group(1)
        path = os.path.join(base, href)
        with open(path, encoding="utf-8") as f:
            css = f.read()
        report["inlined"][href] = os.path.getsize(path)
        if not a.no_minify:
            css = minify(href, css, "css", report)
        return "<style>\n" + inline_assets(css, os.path.dirname(path), report) + "\n</style>"

    if not a.no_minify:
        html = re.sub(r"<!--.*?-->", "", html, flags=re.DOTALL)  # no comments in the shipped file
    html = re.sub(r"""<script[^>]*\ssrc=["']([^"']+)["'][^>]*>\s*</script>""", script_sub, html)
    html = re.sub(r"""<link[^>]*rel=["']stylesheet["'][^>]*href=["']([^"']+)["'][^>]*>""",
                  link_sub, html)
    html = inline_assets(html, base, report)

    os.makedirs(os.path.dirname(os.path.abspath(a.out)), exist_ok=True)
    with open(a.out, "w", encoding="utf-8") as f:
        f.write(html)

    size = os.path.getsize(a.out)
    print(f"{a.out}: {size / 1048576:.2f} MB ({len(report['inlined'])} files inlined)")
    for ref, n in sorted(report["inlined"].items(), key=lambda kv: -kv[1])[:12]:
        print(f"  {n / 1024:8.0f} KB  {ref}")
    for ref, (before, after) in report["minified"].items():
        print(f"  minified {ref}: {before / 1024:.0f} KB -> {after / 1024:.0f} KB")
    for ref, why in report["unminified"].items():
        print(f"  NOT minified {ref}: {why}")
    ok = True
    if report["missing"]:
        # A warning, not a failure: minified libraries contain strings that look like file
        # names (PixiJS has "this.svg"). A real missing asset shows up at once in the browser.
        print("MISSING (left as-is, check each one):", *sorted(report["missing"]), sep="\n  ")
    if report["remote"]:
        ok = False
        print("REMOTE references (a playable must not load at runtime):",
              *sorted(report["remote"]), sep="\n  ")
    if size > a.limit_mb * 1048576:
        ok = False
        print(f"OVER LIMIT: {a.limit_mb} MB")
    sys.exit(0 if ok else 1)


if __name__ == "__main__":
    main()
