#!/usr/bin/env python3
# Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Embed the theme stylesheets as toggleable binary asset blobs in Layer 7.

The themes under src/web/themes/{,generated/} are normally injected into a page at build time. This
generator instead embeds them - minified - as `const char[]` blobs in a registry so the firmware can
serve or switch a theme at runtime (e.g. a `/themes/<name>.css` route, or a picker that remembers the
user's choice). The whole subsystem is behind `DETWS_ENABLE_THEMES`, so a build that does not want
runtime theming links none of it; enabling it embeds the set (each theme is ~1 KB of DROM/flash).

Output: src/network_drivers/application/binary_asset_blobs.{h,cpp} (generated + committed, so the
Arduino-IDE users who never run Python still get it). Re-run after adding/editing a theme.

    python src/web/wizard/gen_theme_blobs.py            # regenerate
    python src/web/wizard/gen_theme_blobs.py --check    # CI: fail if stale
"""

import os
import re
import sys

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
THEMES_DIRS = [
    os.path.normpath(os.path.join(SCRIPT_DIR, "..", "themes")),
    os.path.normpath(os.path.join(SCRIPT_DIR, "..", "themes", "generated")),
]
OUT_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..", "network_drivers", "application"))
BASENAME = "binary_asset_blobs"

BANNER = (
    "// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>\n"
    "// SPDX-License-Identifier: AGPL-3.0-or-later\n"
    "//\n"
    "// GENERATED by src/web/wizard/gen_theme_blobs.py - DO NOT EDIT BY HAND.\n"
    "// Add or edit a theme under src/web/themes/ and re-run the generator."
)


def _restricted():
    """The trademark-named themes (from gen_themes.RESTRICTED) - their blobs are gated for commercial."""
    import importlib.util

    spec = importlib.util.spec_from_file_location("gen_themes", os.path.join(SCRIPT_DIR, "gen_themes.py"))
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return set(mod.RESTRICTED)


RESTRICTED = _restricted()


def minify_css(css):
    css = re.sub(r"/\*.*?\*/", "", css, flags=re.DOTALL)
    css = re.sub(r"\s+", " ", css)
    css = re.sub(r"\s*([{}:;,>])\s*", r"\1", css)
    return css.strip()


_SIMPLE = {"\\": "\\\\", '"': '\\"', "\n": "\\n", "\r": "\\r", "\t": "\\t"}


def escape_segments(text, width=110):
    segs, cur = [], ""
    for ch in text:
        tok = _SIMPLE.get(ch) or ('\\x%02x""' % ord(ch) if ord(ch) < 0x20 else ch)
        if len(cur) + len(tok) > width and cur:
            segs.append(cur)
            cur = ""
        cur += tok
    if cur or not segs:
        segs.append(cur)
    return segs


def c_ident(name):
    return "DETWS_THEME_" + re.sub(r"[^A-Za-z0-9]", "_", name).upper()


def load_themes():
    """Return [(name, minified_css)] for every theme, sorted; a generated/<n> keeps its bare name."""
    seen = {}
    for d in THEMES_DIRS:
        if not os.path.isdir(d):
            continue
        for f in sorted(os.listdir(d)):
            if not f.endswith(".css"):
                continue
            name = f[:-4]
            with open(os.path.join(d, f), encoding="utf-8") as fh:
                seen[name] = minify_css(fh.read())
    return sorted(seen.items())


def render_header(themes):
    guard = "DETERMINISTICESPASYNCWEBSERVER_BINARY_ASSET_BLOBS_H"
    lines = [
        BANNER,
        "",
        "/**",
        " * @file binary_asset_blobs.h",
        " * @brief Layer 7 - toggleable embedded theme stylesheets (DETWS_ENABLE_THEMES).",
        " *",
        " * A registry of minified CSS themes in flash (DROM), for serving/switching a theme at runtime.",
        " * Behind DETWS_ENABLE_THEMES so a build that does not want it links nothing.",
        " */",
        "",
        "#ifndef " + guard,
        "#define " + guard,
        "",
        '#include "ServerConfig.h"',
        "",
        "#if DETWS_ENABLE_THEMES",
        "",
        "#include <stddef.h>",
        "",
        "/** @brief One embedded theme: its name and its minified CSS (NUL-terminated flash string). */",
        "struct DetThemeBlob",
        "{",
        "    const char *name;",
        "    const char *css;",
        "};",
        "",
        "/** @brief The embedded theme registry (sorted by name) and its count. */",
        "extern const DetThemeBlob DETWS_THEME_BLOBS[];",
        "extern const size_t DETWS_THEME_BLOB_COUNT;",
        "",
        "/**",
        " * @brief Look up a theme's CSS by name (exact match).",
        " * @return the NUL-terminated minified CSS, or nullptr if no theme by that name is embedded.",
        " */",
        "const char *detws_theme_css(const char *name);",
        "",
        "#endif // DETWS_ENABLE_THEMES",
        "#endif // " + guard,
    ]
    return "\n".join(lines) + "\n"


def render_source(themes):
    lines = [
        BANNER,
        "",
        '#include "network_drivers/application/binary_asset_blobs.h"',
        "",
        "#if DETWS_ENABLE_THEMES",
        "",
        "#include <string.h>",
        "",
    ]
    for name, css in themes:
        segs = escape_segments(css)
        restricted = name in RESTRICTED
        if restricted:  # a trademark-named theme: keep it in OSS, drop it from a commercial build
            lines.append("#if DETWS_THEMES_INCLUDE_TRADEMARKED")
        lines.append("static const char %s[] =" % c_ident(name))
        lines.append('    "' + '"\n    "'.join(segs) + '";')
        if restricted:
            lines.append("#endif")
        lines.append("")
    lines.append("const DetThemeBlob DETWS_THEME_BLOBS[] = {")
    for name, _ in themes:
        if name in RESTRICTED:
            lines.append("#if DETWS_THEMES_INCLUDE_TRADEMARKED")
            lines.append('    {"%s", %s},' % (name, c_ident(name)))
            lines.append("#endif")
        else:
            lines.append('    {"%s", %s},' % (name, c_ident(name)))
    lines.append("};")
    # sizeof so the count stays correct whether or not the trademark-gated entries are compiled in.
    lines.append("const size_t DETWS_THEME_BLOB_COUNT = sizeof(DETWS_THEME_BLOBS) / sizeof(DETWS_THEME_BLOBS[0]);")
    lines.append("")
    lines.append("const char *detws_theme_css(const char *name)")
    lines.append("{")
    lines.append("    if (!name)")
    lines.append("        return nullptr;")
    lines.append("    for (size_t i = 0; i < DETWS_THEME_BLOB_COUNT; i++)")
    lines.append("        if (strcmp(DETWS_THEME_BLOBS[i].name, name) == 0)")
    lines.append("            return DETWS_THEME_BLOBS[i].css;")
    lines.append("    return nullptr;")
    lines.append("}")
    lines.append("")
    lines.append("#endif // DETWS_ENABLE_THEMES")
    return "\n".join(lines) + "\n"


def generate(write=True):
    themes = load_themes()
    files = {
        os.path.join(OUT_DIR, BASENAME + ".h"): render_header(themes),
        os.path.join(OUT_DIR, BASENAME + ".cpp"): render_source(themes),
    }
    stale = []
    for path, content in files.items():
        old = open(path, encoding="utf-8", newline="").read() if os.path.isfile(path) else None
        if old != content:
            stale.append(os.path.relpath(path))
            if write:
                with open(path, "w", encoding="utf-8", newline="\n") as f:
                    f.write(content)
    return themes, stale


def main():
    check = "--check" in sys.argv[1:]
    themes, stale = generate(write=not check)
    if check:
        if stale:
            sys.stderr.write("STALE (run gen_theme_blobs.py): " + ", ".join(stale) + "\n")
            return 1
        print(f"{len(themes)} theme blobs in sync")
        return 0
    print(f"embedded {len(themes)} theme blobs" + (f" ({', '.join(stale)} changed)" if stale else " (up to date)"))
    return 0


if __name__ == "__main__":
    sys.exit(main())
