#!/usr/bin/env python3
"""Builds page 99 (Engine demos) as a hub + one sub-page per section, from /Volumes/Workspace/Scratch/engine_demos/<name>/.

Each demo folder may hold: *.mp4, *.png / *.jpg stills, caption.txt (first line = title, rest = text).
Media is copied into feedback/img/engine_demos/<name>/ so the page autosync carries it to bashatest.
Hub (99_engine_demos.html): "NEW since your last visit" (browser-remembered), then a tile per section with counts.
Sub-pages (99_<section>.html): newest first; NEW badges; videos load only when played.
Folders without any media (tool folders, runs in progress) are listed in a small "being made" line, not as cards.
Run: python3 _tools/engine_demos_page.py   (coordinator, every check-in)
"""
import html
import os
import shutil
import time

SRC = '/Volumes/Workspace/Scratch/engine_demos'
FB = os.path.expanduser('~/StickBrawl-v3/v4/feedback')
DST = os.path.join(FB, 'img', 'engine_demos')
HUB = os.path.join(FB, '99_engine_demos.html')
MAX_MP4 = 25 * 1024 * 1024  # keep the pages repo light

# (id, title, blurb, matcher)
SECTIONS = [
    ('mega', 'Mega map, showcase + big maps', 'MELTDOWN, the AI-made showcase maps, big maps of any size, engine maps in rooms',
     lambda n: n.startswith(('mega', 'showcase', 'big-map', 'engine-maps-in-rooms', 'ult-', 'bounce', 'demo2', 'online-engine'))),
    ('pieces', 'Map pieces (blocks)', 'Every engine block: zip lines, quicksand, hazards, logic, liquids, closing zone, recipes',
     lambda n: n.startswith(('block-', 'recipe-', 'zipline', 'fix-safe-drop-engine'))),
    ('breakable', 'Breakable maps + chains', 'Destructible ground, materials, floors, breaches, collapse, steel chains',
     lambda n: n.startswith('terrain')),
    ('look', 'Map looks: themes, decor, lava, art, shapes', 'The 24 themes, decorations, the new lava, closed maps, custom art, slopes / moving decks, hardening',
     lambda n: n.startswith(('theme-', 'decor-', 'look-', 'fix-lava', 'art-', 'shape-', 'harden-'))),
    ('films', 'Kill films (animation engine)', 'Film proofs, imported films, film effect blocks, film downloads',
     lambda n: n.startswith(('film', 'proof-six'))),
    ('proofs', 'Proofs (engine = original)', 'Side-by-side proof videos: the engine copy matches the original frame by frame',
     lambda n: n.startswith(('backyard-proof', 'proof-'))),
    ('game', 'Game features', 'Fill online players, safe drop, build-12 features',
     lambda n: n.startswith(('pwf', 'fix-safe-drop', 'fix-'))),
    ('dash', 'Dashboard + server', 'The admin dashboard redesign, tuning, purchases, no-JSON editors',
     lambda n: n.startswith(('dashboard', 'tuning', 'iap'))),
]
OTHER = ('other', 'Other', 'Everything else', None)

CSS = '''
:root{--bg:#17132a;--panel:#221c3d;--ink:#f3efff;--mute:#b9b0dc;--gold:#ffcf3a;--line:#3a3166;--new:#36e0a0}
@media (prefers-color-scheme: light){:root:not([data-theme="dark"]){--bg:#17132a}}
body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.5 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}
main{max-width:1100px;margin:0 auto;padding:24px 16px 80px}
h1,h2,h3{font-family:"Lilita One",system-ui,sans-serif;letter-spacing:.02em;margin:.2em 0}
h1{color:var(--gold);font-size:2.1em}
h2{color:var(--gold)}
a{color:var(--gold)}
.back{color:var(--mute);text-decoration:none}
.lead{color:var(--mute);max-width:760px}
.tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:12px;margin:14px 0}
.tile{display:block;background:var(--panel);border:2px solid var(--line);border-radius:16px;padding:12px;text-decoration:none;color:var(--ink);position:relative}
.tile img{width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:10px;display:block;background:#000}
.tile b{display:block;color:var(--gold);font-family:"Lilita One",system-ui,sans-serif;font-size:1.15em;margin-top:8px}
.tile small{color:var(--mute)}
.badge{position:absolute;top:18px;right:18px;background:var(--new);color:#08261a;font-weight:800;border-radius:999px;padding:2px 10px;font-size:.8em;display:none}
.badge.on{display:inline-block}
.newlist{background:var(--panel);border:2px solid var(--new);border-radius:16px;padding:12px 16px;margin:14px 0}
.newlist ul{margin:.3em 0 0;padding-left:1.1em}
.newlist li{margin:.15em 0}
.demo{background:var(--panel);border:2px solid var(--line);border-radius:16px;padding:16px;margin:16px 0;position:relative}
.demo.isnew{border-color:var(--new)}
.meta{color:var(--mute);font-size:.85em;margin:0 0 8px}
video{width:100%;border-radius:12px;background:#000;margin:6px 0}
.stills{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px}
.stills img{width:100%;border-radius:10px;display:block}
.toc{color:var(--mute);font-size:.9em}
.wait{color:var(--mute);font-size:.9em}
.row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
button{background:var(--line);color:var(--ink);border:0;border-radius:10px;padding:6px 12px;font:inherit;cursor:pointer}
'''

SEEN_JS = '''
<script>
(function(){
  var KEY='p99seen', page=document.body.getAttribute('data-page');
  var seen=0; try{ seen=parseFloat(localStorage.getItem(KEY+':'+page)||'0')||0; }catch(e){}
  var newCount=0;
  document.querySelectorAll('[data-mt]').forEach(function(n){
    var mt=parseFloat(n.getAttribute('data-mt'));
    if(seen && mt>seen){ n.classList.add('isnew'); var b=n.querySelector('.badge'); if(b){b.classList.add('on');} newCount++; }
  });
  document.querySelectorAll('[data-newcount]').forEach(function(n){
    var mts=(n.getAttribute('data-newcount')||'').split(',').map(parseFloat); var c=0;
    mts.forEach(function(m){ if(seen && m>seen) c++; });
    var b=n.querySelector('.badge'); if(b && c){ b.textContent=c+' NEW'; b.classList.add('on'); }
  });
  var nl=document.getElementById('newlist');
  if(nl){
    var items=[].slice.call(nl.querySelectorAll('li[data-mt]'));
    var shown=0;
    items.forEach(function(li){ var mt=parseFloat(li.getAttribute('data-mt')); var keep=seen? mt>seen : true; li.style.display=keep?'':'none'; if(keep) shown++; });
    var h=document.getElementById('newhead');
    if(h) h.textContent = seen ? (shown? shown+' new since your last visit' : 'Nothing new since your last visit') : 'Latest demos (first visit)';
  }
  var mark=function(){ try{ localStorage.setItem(KEY+':'+page, String(Date.now()/1000)); }catch(e){} };
  window.addEventListener('pagehide', mark);
  var btn=document.getElementById('markseen'); if(btn) btn.addEventListener('click', function(){ mark(); location.reload(); });
})();
</script>'''


def demo_dirs():
    if not os.path.isdir(SRC):
        return []
    return [n for n in os.listdir(SRC) if os.path.isdir(os.path.join(SRC, n))]


def section_of(n):
    for s in SECTIONS:
        if s[3](n):
            return s[0]
    return OTHER[0]


def copy_media(name):
    src = os.path.join(SRC, name)
    dst = os.path.join(DST, name)
    media = []
    for f in sorted(os.listdir(src)):
        p = os.path.join(src, f)
        if not os.path.isfile(p):
            continue
        ext = f.lower().rsplit('.', 1)[-1] if '.' in f else ''
        if ext not in ('mp4', 'png', 'jpg', 'jpeg', 'webp'):
            continue
        if ext == 'mp4' and os.path.getsize(p) > MAX_MP4:
            continue
        os.makedirs(dst, exist_ok=True)
        q = os.path.join(dst, f)
        if not os.path.exists(q) or os.path.getmtime(q) < os.path.getmtime(p):
            shutil.copy2(p, q)
        media.append((ext, f'img/engine_demos/{name}/{f}'))
    return media


def caption(name):
    p = os.path.join(SRC, name, 'caption.txt')
    if os.path.exists(p):
        lines = open(p, errors='replace').read().strip().splitlines()
        if lines:
            return lines[0], ' '.join(lines[1:])
    return name.replace('-', ' ').upper(), ''


def mtime(name):
    d = os.path.join(SRC, name)
    ts = [os.path.getmtime(os.path.join(d, f)) for f in os.listdir(d) if os.path.isfile(os.path.join(d, f))]
    return max(ts) if ts else os.path.getmtime(d)


def short(t, n=80):
    t = t.split('. ')[0] if len(t) > n else t
    return t if len(t) <= n else t[:n - 1].rstrip() + '…'


def head(title):
    return (f'<!doctype html><html lang="en"><head><meta charset="utf-8">'
            f'<meta name="viewport" content="width=device-width, initial-scale=1"><title>{html.escape(title)}</title>'
            f'<link href="https://fonts.googleapis.com/css2?family=Lilita+One&display=swap" rel="stylesheet"><style>{CSS}</style></head>')


def card(d):
    body = []
    for v in d['vids']:
        body.append(f'<video src="{html.escape(v)}" controls playsinline muted preload="none" poster="{html.escape(d["pics"][0]) if d["pics"] else ""}"></video>')
    if d['pics']:
        body.append('<div class="stills">' + ''.join(
            f'<a href="{html.escape(p)}"><img src="{html.escape(p)}" loading="lazy" alt=""></a>' for p in d['pics'][:6]) + '</div>')
    when = time.strftime('%d %b %H:%M', time.localtime(d['mt']))
    return (f'<section class="demo" id="{html.escape(d["name"])}" data-mt="{d["mt"]:.0f}"><span class="badge">NEW</span>'
            f'<h2>{html.escape(short(d["title"], 110))}</h2><p class="meta">updated {when}</p>'
            + (f'<p>{html.escape(d["text"])}</p>' if d['text'] else '') + ''.join(body) + '</section>')


def main():
    demos = []
    for n in demo_dirs():
        media = copy_media(n)
        t, x = caption(n)
        demos.append({'name': n, 'sec': section_of(n), 'title': t, 'text': x, 'mt': mtime(n),
                      'vids': [m for e, m in media if e == 'mp4'], 'pics': [m for e, m in media if e != 'mp4']})
    secs = SECTIONS + [OTHER]
    made = time.strftime('%d %b %Y, %H:%M')
    tiles = []
    for sid, title, blurb, _ in secs:
        ds = sorted([d for d in demos if d['sec'] == sid and (d['vids'] or d['pics'])], key=lambda d: -d['mt'])
        wip = [d for d in demos if d['sec'] == sid and not (d['vids'] or d['pics'])]
        if not ds and not wip:
            continue
        page = f'99_{sid}.html'
        cards = ''.join(card(d) for d in ds) or '<p class="wait">Nothing to show yet.</p>'
        wipline = (f'<p class="wait">Being made: {html.escape(", ".join(d["name"] for d in wip))}</p>' if wip else '')
        toc = ' · '.join(f'<a href="#{html.escape(d["name"])}">{html.escape(d["title"][:40])}</a>' for d in ds[:40])
        open(os.path.join(FB, page), 'w').write(
            head(title) + f'<body data-page="{sid}"><main><a class="back" href="99_engine_demos.html">← all engine demos</a>'
            f'<h1>{html.escape(title)}</h1><p class="lead">{html.escape(blurb)}. Newest first; green = new since your last visit here. Page made {made}.</p>'
            f'<div class="row"><button id="markseen">Mark all as seen</button></div><p class="toc">{toc}</p>{wipline}{cards}</main>{SEEN_JS}</body></html>')
        thumb = ds[0]['pics'][0] if ds and ds[0]['pics'] else ''
        mts = ','.join(f'{d["mt"]:.0f}' for d in ds)
        newest = time.strftime('%d %b %H:%M', time.localtime(ds[0]['mt'])) if ds else '—'
        tiles.append(f'<a class="tile" href="{page}" data-newcount="{mts}"><span class="badge">NEW</span>'
                     + (f'<img src="{html.escape(thumb)}" loading="lazy" alt="">' if thumb else '')
                     + f'<b>{html.escape(title)}</b><small>{len(ds)} demos · newest {newest}</small><br><small>{html.escape(blurb)}</small></a>')
    latest = sorted([d for d in demos if d['vids'] or d['pics']], key=lambda d: -d['mt'])[:15]
    newlist = ''.join(f'<li data-mt="{d["mt"]:.0f}"><a href="99_{d["sec"]}.html#{html.escape(d["name"])}">{html.escape(short(d["title"]))}</a>'
                      f' <small class="wait">· {time.strftime("%d %b %H:%M", time.localtime(d["mt"]))}</small></li>' for d in latest)
    open(HUB, 'w').write(
        head('Engine demos') + '<body data-page="hub"><main><a class="back" href="36_index.html">← all pages</a>'
        f'<h1>Engine demos</h1><p class="lead">Videos and stills from the real game, split into sections. Page made {made}.</p>'
        f'<div class="newlist"><h3 id="newhead">New</h3><ul id="newlist">{newlist}</ul>'
        '<div class="row"><button id="markseen">Mark all as seen</button></div></div>'
        f'<div class="tiles">{"".join(tiles)}</div></main>{SEEN_JS}</body></html>')
    print('page 99:', len(demos), 'demo folders,', len(tiles), 'sections')


if __name__ == '__main__':
    main()
