# -*- coding: utf-8 -*-
"""逐页转写网页生成器：源 PPT 每页 → 一个网页（原页高清图 + 真实文字层 + 本页素材/文字稿）。

用法：python _build_faithful.py <_conv_work 目录> <输出目录> "<课程名>" "<章节名>"
依赖：_data/slides.json、assets/page/slide-*.png、assets/media/*
"""
import json
import os
import shutil
import sys

CSS = """
:root{--ink:#1f2937;--navy:#1a365d;--blue:#2b6cb0;--line:#cbd5e0;--bg:#f5f7fa;}
*{box-sizing:border-box}
body{margin:0;font-family:"Microsoft YaHei","PingFang SC","Hiragino Sans GB",sans-serif;color:var(--ink);background:var(--bg)}
header{display:flex;align-items:center;gap:14px;padding:8px 16px;background:var(--navy);color:#fff;position:sticky;top:0;z-index:20}
header .t{font-weight:700}
header .sp{flex:1}
header a,header button{background:rgba(255,255,255,.14);color:#fff;border:0;border-radius:6px;padding:6px 12px;
  font-size:13px;text-decoration:none;cursor:pointer;font-family:inherit}
header button.on{background:#fff;color:var(--navy);font-weight:700}
main{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:16px;padding:16px;align-items:start}
@media (max-width:1000px){main{grid-template-columns:1fr}}
.stage{position:relative;width:100%;aspect-ratio:16/9;background:#fff;border:1px solid var(--line);
  border-radius:10px;overflow:hidden;container-type:size}
.stage img.page{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:#fff}
.layer{position:absolute;inset:0}
.layer .tb{position:absolute;font-size:calc(var(--fs) * 1cqw / 1);line-height:1.25;color:#111;
  background:rgba(255,255,255,.88);border:1px dashed rgba(43,108,176,.55);border-radius:4px;
  padding:.25em .4em;overflow:hidden}
.layer .tb b{font-weight:700}
body.mode-raw .layer{display:none}
body.mode-over .layer{display:block}
body.mode-text .layer{display:block}
body.mode-text .stage img.page{display:none}
body.mode-text .stage{aspect-ratio:auto;min-height:60vh}
body.mode-text .layer{position:relative;inset:auto;display:flex;flex-direction:column;gap:10px;padding:14px}
body.mode-text .layer .tb{position:relative;left:auto !important;top:auto !important;width:auto !important;
  background:#fff;border-style:solid;font-size:15px}
aside{background:#fff;border:1px solid var(--line);border-radius:10px;padding:12px 14px}
aside h3{margin:6px 0 8px;font-size:14px;color:var(--navy)}
aside ol{margin:0;padding-left:20px;font-size:13px;line-height:1.6}
aside ol li{margin:4px 0;padding-bottom:4px;border-bottom:1px dashed #edf0f4}
.media{display:flex;flex-wrap:wrap;gap:8px;margin-top:6px}
.media a{display:block;border:1px solid var(--line);border-radius:6px;overflow:hidden;background:#fff}
.media img{display:block;width:96px;height:60px;object-fit:contain;background:#fafbfc}
.notes{font-size:12px;color:#4a5568;background:#f7fafc;border-left:3px solid var(--blue);padding:8px 10px;margin-top:8px;border-radius:0 6px 6px 0}
.pager{display:flex;justify-content:space-between;padding:0 16px 24px}
.pager a{background:var(--navy);color:#fff;text-decoration:none;padding:10px 20px;border-radius:8px;font-size:14px}
.pager a.dis{opacity:.35;pointer-events:none}
/* 目录页 */
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:14px;padding:16px}
.card{background:#fff;border:1px solid var(--line);border-radius:10px;overflow:hidden;text-decoration:none;color:inherit}
.card img{display:block;width:100%;aspect-ratio:16/9;object-fit:contain;background:#fafbfc}
.card .cap{padding:8px 10px;font-size:13px}
.card .no{display:inline-block;min-width:28px;color:var(--blue);font-weight:700}
#q{width:100%;padding:9px 12px;border:1px solid var(--line);border-radius:8px;font-size:14px;font-family:inherit}
"""

JS = """
(function(){
  var modes=['raw','over','text'], labels={raw:'原页',over:'叠字',text:'纯文字'};
  var body=document.body;
  function setMode(m){body.className='mode-'+m; document.querySelectorAll('header [data-mode]').forEach(function(b){
      b.classList.toggle('on', b.dataset.mode===m);}); try{localStorage.setItem('slideMode',m);}catch(e){}}
  document.querySelectorAll('header [data-mode]').forEach(function(b){
      b.addEventListener('click',function(){setMode(b.dataset.mode);});});
  try{var m=localStorage.getItem('slideMode'); if(m) setMode(m); else setMode('raw');}catch(e){setMode('raw');}
  var prev=document.querySelector('.pager a.prev'), next=document.querySelector('.pager a.next');
  document.addEventListener('keydown',function(e){
    if(e.target.tagName==='INPUT') return;
    if(e.key==='ArrowLeft'&&prev&&!prev.classList.contains('dis')) location.href=prev.href;
    if(e.key==='ArrowRight'&&next&&!next.classList.contains('dis')) location.href=next.href;
    if(e.key==='Home') location.href='index.html';
  });
  var lock=false;
  window.addEventListener('wheel',function(e){
    if(e.target.closest('aside')) return;
    if(lock) return; if(Math.abs(e.deltaY)<18) return; lock=true; setTimeout(function(){lock=false;},450);
    if(e.deltaY>0&&next&&!next.classList.contains('dis')) location.href=next.href;
    if(e.deltaY<0&&prev&&!prev.classList.contains('dis')) location.href=prev.href;
  },{passive:true});
})();
"""

SEARCH_JS = """
(function(){
  var q=document.getElementById('q'); if(!q) return;
  var cards=Array.prototype.slice.call(document.querySelectorAll('.card'));
  q.addEventListener('input',function(){
    var v=q.value.trim().toLowerCase();
    cards.forEach(function(c){
      var hay=(c.dataset.text||'').toLowerCase()+' '+(c.dataset.title||'').toLowerCase();
      c.style.display=(!v||hay.indexOf(v)>=0)?'':'none';
    });
  });
})();
"""


def esc(s):
    return (s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;'))


def build(work, outdir, course, chapter):
    data = json.load(open(os.path.join(work, '_data', 'slides.json'), encoding='utf-8'))
    SW, SH = data['slide_w'], data['slide_h']
    slides = data['slides']
    n = len(slides)
    if os.path.isdir(outdir):
        shutil.rmtree(outdir)
    os.makedirs(os.path.join(outdir, 'assets'), exist_ok=True)
    # 统一重命名为 slide-001.png 形式，与 HTML 引用一致（渲染出来的可能是 slide-01.png）
    page_src = os.path.join(work, 'assets', 'page')
    page_dst = os.path.join(outdir, 'assets', 'page')
    os.makedirs(page_dst, exist_ok=True)
    pages = sorted(os.listdir(page_src))
    if len(pages) != n:
        raise SystemExit('原页图数量 %d 与页数 %d 不一致' % (len(pages), n))
    for k, name in enumerate(pages, 1):
        shutil.copyfile(os.path.join(page_src, name),
                        os.path.join(page_dst, 'slide-%03d%s' % (k, os.path.splitext(name)[1])))
    shutil.copytree(os.path.join(work, 'assets', 'media'), os.path.join(outdir, 'assets', 'media'))
    open(os.path.join(outdir, 'app.css'), 'w', encoding='utf-8').write(CSS)
    open(os.path.join(outdir, 'app.js'), 'w', encoding='utf-8').write(JS + SEARCH_JS)

    def head(title, extra=''):
        return ('<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8">'
                '<meta name="viewport" content="width=device-width,initial-scale=1">'
                '<title>%s</title><link rel="stylesheet" href="app.css"></head><body class="mode-raw">'
                % esc(title))

    def header(page=None):
        h = ['<header><span class="t">%s</span><span>%s</span><span class="sp"></span>' % (esc(course), esc(chapter))]
        if page:
            h.append('<span>第 %d / %d 页</span>' % (page, n))
            for m, lb in (('raw', '原页'), ('over', '叠字'), ('text', '纯文字')):
                h.append('<button data-mode="%s">%s</button>' % (m, lb))
        h.append('<a href="index.html">目录</a></header>')
        return ''.join(h)

    # 单页
    for s in slides:
        i = s['index']
        parts = [head('%s 第 %d 页' % (chapter, i)), header(i), '<main><section class="stage">']
        parts.append('<img class="page" src="assets/page/slide-%03d.png" alt="第 %d 页原图">' % (i, i))
        parts.append('<div class="layer">')
        for t in s['texts']:
            fs = max(1.6, t['size'] / (72.0 * SW) * 100.0)      # pt → 幻灯片宽度百分比
            left = max(0.0, t['left'] / SW * 100)
            top = max(0.0, t['top'] / SH * 100)
            w = max(6.0, min(100 - left, t['w'] / SW * 100 * 1.06))
            body_html = '<br>'.join(esc(x) for x in t['lines']) if t['lines'] else esc(t['text'])
            if t['bold']:
                body_html = '<b>%s</b>' % body_html
            parts.append('<div class="tb" style="--fs:%.2f;left:%.2f%%;top:%.2f%%;width:%.2f%%">%s</div>'
                         % (fs, left, top, w, body_html))
        parts.append('</div></section><aside>')
        parts.append('<h3>本页文字稿（%d 段）</h3><ol>' % len(s['texts']))
        for t in s['texts']:
            parts.append('<li>%s</li>' % esc(t['text']).replace('\n', '<br>'))
        if not s['texts']:
            parts.append('<li>（本页无文字，为纯图形页）</li>')
        parts.append('</ol>')
        parts.append('<h3>本页素材（%d 张，点击下载原图）</h3><div class="media">' % len(s['pics']))
        for p in s['pics']:
            parts.append('<a href="assets/media/%s" download title="%s"><img src="assets/media/%s" alt="%s"></a>'
                         % (p['file'], p['file'], p['file'], p['file']))
        if not s['pics']:
            parts.append('<span style="font-size:12px;color:#718096">本页无位图（图形为 PPT 矢量绘制，已包含在原页图中）</span>')
        parts.append('</div>')
        if s['notes']:
            parts.append('<div class="notes"><b>演讲者备注：</b>%s</div>' % esc(s['notes']).replace('\n', '<br>'))
        parts.append('</aside></main><div class="pager">')
        parts.append('<a class="prev%s" href="%s">← 上一页</a>' % (' dis' if i == 1 else '',
                                                                    'index.html' if i == 1 else 'slide-%03d.html' % (i - 1)))
        parts.append('<a class="next%s" href="%s">下一页 →</a>' % (' dis' if i == n else '',
                                                                    'index.html' if i == n else 'slide-%03d.html' % (i + 1)))
        parts.append('</div><script src="app.js"></script></body></html>')
        open(os.path.join(outdir, 'slide-%03d.html' % i), 'w', encoding='utf-8').write(''.join(parts))

    # 目录
    idx = [head('%s %s 目录' % (course, chapter)), header(), '<div style="padding:16px 16px 0">',
           '<input id="q" placeholder="输入关键词搜索本页文字（如：缺陷、等价类、TDD）">',
           '</div><div class="grid">']
    for s in slides:
        txt = ' '.join(t['text'] for t in s['texts'])
        idx.append('<a class="card" href="slide-%03d.html" data-title="%s" data-text="%s">'
                   '<img loading="lazy" src="assets/page/slide-%03d.png" alt="第 %d 页">'
                   '<div class="cap"><span class="no">%02d</span>%s</div></a>'
                   % (s['index'], esc(s['title']), esc(txt), s['index'], s['index'],
                      s['index'], esc(s['title'])))
    idx.append('</div><div style="padding:8px 16px 28px;color:#718096;font-size:12px">'
               '共 %d 页，与源课件页数一致；每页含原页高清图、可选中文字层、本页素材原图与文字稿。'
               '</div><script src="app.js"></script></body></html>' % n)
    open(os.path.join(outdir, 'index.html'), 'w', encoding='utf-8').write(''.join(idx))
    return n


if __name__ == '__main__':
    work, outdir = sys.argv[1], sys.argv[2]
    course = sys.argv[3] if len(sys.argv) > 3 else '软件测试课程设计'
    chapter = sys.argv[4] if len(sys.argv) > 4 else '第 1 章 引论'
    n = build(work, outdir, course, chapter)
    print('已生成 %d 页 + 目录 -> %s' % (n, outdir))
