#!/usr/bin/env python3
"""Builds a sizes attribute and a srcset width ladder from the CSS layout. Standard library only.
Usage: python3 sizes_calc.py --rule "(min-width: 1024px) 400px" --rule "(min-width: 640px) 50vw" --default 100vw --max 1600
Options: --viewports 360,768,1024,1440  --dpr 2  --max <widest source px>  --limit 6  --merge 1.2
Slot widths accept px, vw and calc(100vw - Npx). Rules are read top to bottom, first match wins, as in sizes.
Defaults (dpr 2, limit 6, merge 1.2) are choices, not rules; change them to fit the project."""
import argparse, math, re, sys

def slot(expr, vw):
    e = expr.strip().lower()
    m = re.fullmatch(r"(\d+(?:\.\d+)?)px", e)
    if m: return float(m.group(1))
    m = re.fullmatch(r"(\d+(?:\.\d+)?)vw", e)
    if m: return vw * float(m.group(1)) / 100
    m = re.fullmatch(r"calc\(\s*(\d+(?:\.\d+)?)vw\s*([+-])\s*(\d+(?:\.\d+)?)px\s*\)", e)
    if m:
        d = float(m.group(3)); return vw * float(m.group(1)) / 100 + (d if m.group(2) == "+" else -d)
    raise ValueError(f"unsupported width expression: {expr!r} (use px, vw or calc(Nvw - Npx))")

def cond_ok(cond, vw):
    ok = True
    for kind, val in re.findall(r"\((min|max)-width:\s*(\d+(?:\.\d+)?)px\)", cond.lower()):
        ok &= vw >= float(val) if kind == "min" else vw <= float(val)
    if not re.search(r"\((min|max)-width", cond.lower()): raise ValueError(f"unsupported media condition: {cond!r}")
    return ok

def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--rule", action="append", default=[], help='"(min-width: Npx) <width>", repeatable')
    ap.add_argument("--default", required=True, help="slot width when no rule matches")
    ap.add_argument("--viewports", default="360,768,1024,1440")
    ap.add_argument("--dpr", type=float, default=2.0)
    ap.add_argument("--max", type=int, required=True, help="width in px of the largest source file you can serve")
    ap.add_argument("--limit", type=int, default=6)
    ap.add_argument("--merge", type=float, default=1.2)
    a = ap.parse_args()
    rules = []
    for r in a.rule:
        m = re.fullmatch(r"\s*(\(.*\))\s+(\S.*)", r)
        if not m: print(f"bad rule: {r!r}", file=sys.stderr); return 2
        rules.append((m.group(1), m.group(2)))
    try:
        need, rows = set(), []
        for vw in (int(v) for v in a.viewports.split(",")):
            expr = next((w for c, w in rules if cond_ok(c, vw)), a.default)
            px = slot(expr, vw)
            if px <= 0: raise ValueError(f"slot width is {px:.0f}px at {vw}px viewport")
            for d in (1.0, a.dpr):
                need.add(min(a.max, math.ceil(px * d / 10) * 10))
            rows.append((vw, expr, round(px), min(a.max, math.ceil(px * a.dpr / 10) * 10)))
    except ValueError as e:
        print(f"error: {e}", file=sys.stderr); return 2
    ladder = []
    for w in sorted(need):
        if not ladder or w >= ladder[-1] * a.merge: ladder.append(w)
        else: ladder[-1] = w   # keep the larger so no viewport is under-served
    while len(ladder) > a.limit:
        i = min(range(1, len(ladder)), key=lambda k: ladder[k] / ladder[k - 1]); del ladder[i - 1]
    print("viewport  slot          css px  px at dpr")
    for vw, expr, px, need_px in rows: print(f"{vw:<9} {expr:<13} {px:<7} {need_px}")
    print("sizes=\"" + ", ".join([f"{c} {w}" for c, w in rules] + [a.default]) + "\"")
    print("srcset widths: " + ", ".join(f"{w}w" for w in ladder))
    return 0

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