#!/usr/bin/env python3
"""Derive the Course I slice manifest from measured geometry, then assemble the pilot.

The cut lines below are NOT eyeballed. They were fitted from the hero's own pixels:
each slab's bottom edge tracked column-by-column through the void gaps, least-squares
lines through the left and right arms, tip at their intersection. Symmetry of the two
arms came out within 0-10% on every slab, which is the check that the fit is real.
Re-derive with tools/fit-geometry.py if the hero is ever regenerated.
"""
import base64, json, pathlib, re, sys

ROOT = pathlib.Path(__file__).resolve().parent.parent
HERO = ROOT / "assets/dist/docker-layers.webp"

# measured bottom silhouettes: left corner -> bottom tip -> right corner (percent of frame)
CUTS = {
    "writable": dict(l=(27.75, 27.24), tip=(50.77, 40.73), r=(72.00, 26.87)),
    "code":     dict(l=(28.38, 41.34), tip=(49.76, 56.15), r=(71.50, 41.02)),
    "deps":     dict(l=(27.88, 56.36), tip=(49.48, 76.21), r=(71.88, 56.30)),
    "base":     dict(l=(28.12, 74.41), tip=(49.95, 94.20), r=(71.75, 74.40)),
}
EPS  = 0.7    # regions grow UPWARD only — the slab in front always covers the overlap.
              # Growing downward would drag a copy of the slab behind into the slice.
NEST = 0.75   # fraction of each gap closed when collapsed

ORDER = ["base", "deps", "code", "writable"]          # paint order: back -> front
LABEL = {
    "base":     ("Base image", "The OS layer everything else is stacked on. Pull it once, "
                               "share it across every image on the host."),
    "deps":     ("Dependencies", "Your packages and runtime. Change these and every layer "
                                 "above has to be rebuilt."),
    "code":     ("Application code", "Your actual program — usually the smallest layer, and "
                                     "the one that changes every single build."),
    "writable": ("Writable layer", "Created fresh per container, thrown away when it dies. "
                                   "Anything written here is not in the image."),
}


def V(c):        return [list(c["l"]), list(c["tip"]), list(c["r"])]
def up(v, e):    return [[p[0], round(p[1] - e, 2)] for p in v]


def band(top, bot):
    """Region between two silhouettes; sides run to the frame edge, which is pure void."""
    p = []
    p += ([[0, top[0][1]]] + top + [[100, top[2][1]]]) if top else [[0, 0], [100, 0]]
    p += ([[100, bot[2][1]]] + bot[::-1] + [[0, bot[0][1]]]) if bot else [[100, 100], [0, 100]]
    return p


def manifest():
    tips = {k: CUTS[k]["tip"][1] for k in CUTS}
    dy, acc = {}, 0.0
    for i, k in enumerate(ORDER):
        if i:
            acc += (tips[ORDER[i - 1]] - tips[k]) * NEST
        dy[k] = round(acc, 2)

    regions = {
        "base":     band(up(V(CUTS["deps"]), EPS),     None),
        "deps":     band(up(V(CUTS["code"]), EPS),     V(CUTS["deps"])),
        "code":     band(up(V(CUTS["writable"]), EPS), V(CUTS["code"])),
        "writable": band(None,                          V(CUTS["writable"])),
    }
    return {
        "scene": "course-I-docker-layers", "eps": EPS, "nest": NEST,
        "source": "assets/dist/docker-layers.webp",
        "parts": [{"name": k, "title": LABEL[k][0], "blurb": LABEL[k][1],
                   "anchor": round(CUTS[k]["tip"][1] - 6, 2),
                   "points": regions[k], "dy": dy[k]} for k in ORDER],
    }


def main():
    if not HERO.exists():
        sys.exit(f"missing hero: {HERO}")
    man = manifest()
    (ROOT / "pilot/manifest.json").write_text(json.dumps(man, indent=1))

    hero = "data:image/webp;base64," + base64.b64encode(HERO.read_bytes()).decode()
    out = (ROOT / "pilot/course-1.tpl.html").read_text() \
        .replace("__MANIFEST__", json.dumps(man, separators=(",", ":"))) \
        .replace("__HERO__", hero)
    out = (f"<script>{(ROOT / 'gsap.min.js').read_text()}</script>\n"
           f"<script>{(ROOT / 'st.min.js').read_text()}</script>\n" + out)

    dest = ROOT / "pilot/course-1.html"
    dest.write_text(out)

    blocks = re.findall(r"<script>([\s\S]*?)</script>", out)
    print(f"built {dest.relative_to(ROOT)}  {len(out)/1024:.0f}KB  "
          f"({len(blocks)} script blocks, hero {len(hero)/1024:.0f}KB inlined)")
    for name, part in zip(ORDER, man["parts"]):
        print(f"  {name:<9} {len(part['points']):2d} pts   collapse travel {part['dy']:5.2f}%")


if __name__ == "__main__":
    main()
