#!/usr/bin/env python3
"""
把 5.5 寸手持端设计稿（HTML 原型）渲染成截图，供逐像素比对。

设计稿是带 Tailwind + JS 的交互原型：列表页、Tab、详情页、筛选弹层往往挤在同一个
HTML 里，靠 JS 函数切换。只读源码容易漏掉视觉细节（间距、圆角、真实配色、
以及 JS 里 id 映射写错导致的字段错位），所以务必渲染成图来看。

用法：
    # 1) 先列出 HTML 里可用的状态切换函数与元素 id，据此决定要截哪些状态
    python3 design_shots.py --html 出库交接.html --list

    # 2) 渲染：默认状态 + 注入 JS 触发的各个状态
    python3 design_shots.py --html 出库交接.html --out /tmp/shots \
        --state "01_列表:" \
        --state "02_已交接Tab:switchTab('shipped');" \
        --state "03_详情:showDetail({id:'PMC1',status:'已交接'});" \
        --state "04_筛选弹层:toggleFilterDrawer(true);"

每个 --state 形如 "名称:JS"，JS 可为空表示页面初始态。
输出 <out>/<名称>.png，同时保留注入后的 <名称>.html 便于复查。
"""

import argparse
import os
import re
import shutil
import subprocess
import sys
import urllib.parse

CHROME_CANDIDATES = [
    "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
    "/Applications/Chromium.app/Contents/MacOS/Chromium",
    shutil.which("google-chrome") or "",
    shutil.which("chromium") or "",
    shutil.which("chromium-browser") or "",
]


def find_chrome() -> str:
    for path in CHROME_CANDIDATES:
        if path and os.path.exists(path):
            return path
    sys.exit(
        "找不到 Chrome/Chromium。请安装，或用 --chrome 指定可执行文件路径。"
    )


def list_hooks(html: str) -> None:
    """列出可用于构造状态的 JS 函数和元素 id。"""
    funcs = sorted(set(re.findall(r"function\s+([A-Za-z_$][\w$]*)\s*\(", html)))
    onclicks = sorted(set(re.findall(r'onclick="([^"]+)"', html)))
    ids = sorted(set(re.findall(r'id="([^"]+)"', html)))

    print("== JS 函数（可直接注入调用）==")
    for f in funcs:
        print(f"  {f}()")
    print("\n== onclick 表达式（含真实参数，照抄最省事）==")
    for o in onclicks[:40]:
        print(f"  {o}")
    if len(onclicks) > 40:
        print(f"  ... 其余 {len(onclicks) - 40} 条略")
    print("\n== 元素 id（判断有哪些页面/弹层）==")
    print("  " + ", ".join(ids))


def render(chrome: str, html: str, name: str, js: str, out_dir: str,
           width: int, height: int, scale: float, budget: int) -> str:
    # 等 load 之后再触发，确保 CDN 的 Tailwind/Iconify 已生效
    inject = (
        "<script>window.addEventListener('load',function(){"
        f"setTimeout(function(){{{js}}},300);}});</script>"
    )
    page_path = os.path.join(out_dir, f"{name}.html")
    with open(page_path, "w", encoding="utf-8") as fp:
        fp.write(html.replace("</body>", inject + "</body>"))

    png_path = os.path.join(out_dir, f"{name}.png")
    subprocess.run(
        [
            chrome,
            "--headless=new",
            "--disable-gpu",
            "--hide-scrollbars",
            f"--force-device-scale-factor={scale}",
            f"--window-size={width},{height}",
            f"--virtual-time-budget={budget}",
            f"--screenshot={png_path}",
            "file://" + urllib.parse.quote(os.path.abspath(page_path)),
        ],
        capture_output=True,
    )
    return png_path


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("--html", required=True, help="设计稿 HTML 路径")
    ap.add_argument("--out", default="/tmp/design_shots", help="输出目录")
    ap.add_argument("--state", action="append", default=[],
                    help='形如 "名称:JS"，可重复；JS 为空表示初始态')
    ap.add_argument("--list", action="store_true",
                    help="只列出可用的 JS 函数 / onclick / id，不截图")
    ap.add_argument("--chrome", default="", help="Chrome 可执行文件路径")
    # 原型容器一般是 375×812 的手机壳，留出边距后 460×900 刚好完整收进去
    ap.add_argument("--width", type=int, default=460)
    ap.add_argument("--height", type=int, default=900)
    ap.add_argument("--scale", type=float, default=2.0, help="缩放倍数，2 便于看清细节")
    ap.add_argument("--budget", type=int, default=9000,
                    help="虚拟时间预算(ms)，需覆盖 CDN 脚本加载")
    args = ap.parse_args()

    with open(args.html, encoding="utf-8") as fp:
        html = fp.read()

    if args.list:
        list_hooks(html)
        return

    chrome = args.chrome or find_chrome()
    os.makedirs(args.out, exist_ok=True)

    states = args.state or ["01_default:"]
    for spec in states:
        name, _, js = spec.partition(":")
        png = render(chrome, html, name.strip(), js, args.out,
                     args.width, args.height, args.scale, args.budget)
        ok = os.path.exists(png) and os.path.getsize(png) > 0
        size = os.path.getsize(png) if ok else 0
        print(f"{'✓' if ok else '✗'} {name.strip():<24} {png} ({size} bytes)")

    print(f"\n用 Read 工具逐张查看 {args.out}/*.png，再开始写布局。")


if __name__ == "__main__":
    main()
