#!/usr/bin/env python3
"""
Generate preview HTML from render HTML — deterministic CLI for hyper-animator.

Reads the render HTML, injects preview controls (start overlay, progress bar,
page numbers, audio panel, time display, keyboard nav, audio-visual sync).
Output is always consistent — no manual editing needed.

Usage:
    python3 scripts/preview-gen.py \\
        --input hyperframes-output/<name>.html \\
        --output hyperframes-output/<name>-preview.html \\
        --scenes 0,5,10,15,20,25,30 \\
        --duration 30
"""
import argparse
import re
import sys


PREVIEW_CSS = """
    /* ── Preview controls (only in preview file) ── */
    .hf-start-overlay { position: fixed; inset: 0; z-index: 20000; background: rgba(0,0,0,0.85); display: flex; justify-content: center; align-items: center; cursor: pointer; }
    .hf-start-btn { padding: 16px 48px; border: 1px solid rgba(255,255,255,0.3); border-radius: 4px; color: rgba(255,255,255,0.8); font-family: 'JetBrains Mono', monospace; font-size: 14px; background: transparent; cursor: pointer; }
    .hf-start-btn:hover { border-color: rgba(255,255,255,0.7); }
    .hf-preview-pages { position: fixed; bottom: 10px; left: 0; right: 0; z-index: 10000; text-align: center; font-family: 'JetBrains Mono', monospace; font-size: 12px; color: rgba(255,255,255,0.45); }
    .hf-preview-time { position: fixed; bottom: 10px; right: 12px; z-index: 10000; font-family: 'JetBrains Mono', monospace; font-size: 12px; color: rgba(255,255,255,0.4); }
    .hf-preview-bar { position: fixed; bottom: 0; left: 0; right: 0; z-index: 10000; height: 4px; background: rgba(255,255,255,0.08); cursor: pointer; }
    .hf-preview-bar-fill { height: 100%; background: rgba(255,255,255,0.5); width: 0%; }
    .hf-preview-audio { position: fixed; top: 12px; right: 12px; z-index: 10000; display: flex; align-items: center; gap: 8px; font-family: 'JetBrains Mono', monospace; font-size: 11px; color: rgba(255,255,255,0.4); }
    .hf-preview-audio input[type=range] { width: 60px; accent-color: rgba(255,255,255,0.5); }
"""

PREVIEW_HTML = """
  <!-- === BROWSER PREVIEW UI === -->
  <div class="hf-start-overlay" id="hf-start-overlay">
    <button class="hf-start-btn" id="hf-start-btn">&#9654; 点击开始预览</button>
  </div>
  <div class="hf-preview-audio" id="preview-audio-panel">
    <span>&#9835;</span>
    <input type="range" id="preview-volume" min="0" max="1" step="0.05" value="0.85" />
  </div>
  <div class="hf-preview-time" id="preview-time">00:00 / 00:30</div>
  <div id="preview-bottom">
    <div class="hf-preview-pages" id="preview-pages">1 / 7</div>
    <div class="hf-preview-bar" id="preview-bar">
      <div class="hf-preview-bar-fill" id="preview-bar-fill"></div>
      <div class="hf-preview-bar-dots" id="preview-bar-dots"></div>
    </div>
  </div>
"""

PREVIEW_JS_TEMPLATE = """
      // ── BROWSER PREVIEW: multi-audio sync + controls ──
      (function() {{
        var SCENES = [{scenes}];
        var DURATION = {duration};
        var currentScene = 0;
        var pagesEl = document.getElementById('preview-pages');
        var barFill = document.getElementById('preview-bar-fill');
        var bar = document.getElementById('preview-bar');
        var timeEl = document.getElementById('preview-time');
        var volSlider = document.getElementById('preview-volume');

        // Collect ALL audio elements (BGM + narration scenes)
        var allAudio = Array.prototype.slice.call(document.querySelectorAll('audio.clip'));
        var sceneAudioMap = {{}}; // scene-index -> audio elements active at that scene

        // Build scene-audio mapping: for each audio element, find which scenes it covers
        allAudio.forEach(function(a) {{
          var aStart = parseFloat(a.getAttribute('data-start') || '0');
          var aEnd = aStart + parseFloat(a.getAttribute('data-duration') || '0');
          // BGM (track-index=10, covers full duration) -> always active
          // Narration (track-index=11, per-scene) -> active only during its scene
          for (var s = 0; s < SCENES.length; s++) {{
            var sceneStart = SCENES[s];
            var sceneEnd = s+1 < SCENES.length ? SCENES[s+1] : DURATION;
            if (aStart < sceneEnd && aEnd > sceneStart) {{
              if (!sceneAudioMap[s]) sceneAudioMap[s] = [];
              sceneAudioMap[s].push(a);
            }}
          }}
        }});

        // Seek ALL audio elements (not just bgm-track)
        function seekBoth(s) {{
          tl.seek(s);
          allAudio.forEach(function(a) {{
            var aStart = parseFloat(a.getAttribute('data-start') || '0');
            var aDur = parseFloat(a.getAttribute('data-duration') || '0');
            if (a.readyState >= 1 && s >= aStart && s <= aStart + aDur + 1) {{
              a.currentTime = Math.max(0, s - aStart);
            }}
          }});
        }}

        function playBoth() {{
          tl.play();
          allAudio.forEach(function(a) {{
            var aStart = parseFloat(a.getAttribute('data-start') || '0');
            var aDur = parseFloat(a.getAttribute('data-duration') || '0');
            var t = tl.time();
            if (a.readyState >= 1 && t >= aStart && t <= aStart + aDur + 1) {{
              a.currentTime = Math.max(0, t - aStart);
              var p = a.play(); if (p && p.then) p.catch(function(){{}});
            }}
          }});
        }}

        function pauseBoth() {{
          tl.pause();
          allAudio.forEach(function(a) {{ a.pause(); }});
        }}

        document.getElementById('hf-start-btn').addEventListener('click', function() {{
          document.getElementById('hf-start-overlay').style.display = 'none';
          playBoth();
        }});

        if (volSlider)
          volSlider.addEventListener('input', function() {{
            allAudio.forEach(function(a) {{ a.volume = parseFloat(volSlider.value); }});
          }});

        SCENES.forEach(function(s, i) {{
          var dot = document.createElement('div');
          dot.style.cssText = 'position:absolute;left:'+(s/DURATION*100)+'%;width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,0.3);';
          dot.id = 'preview-dot-'+i;
          document.getElementById('preview-bar-dots').appendChild(dot);
        }});

        function updatePreview() {{
          var t = tl.time(), pct = Math.min(100, t/DURATION*100);
          barFill.style.width = pct+'%';
          for (var i = SCENES.length-1; i >= 0; i--) {{ if (t >= SCENES[i]) {{ currentScene = i; break; }} }}
          pagesEl.textContent = (currentScene+1)+' / '+SCENES.length;
          document.querySelectorAll('#preview-bar-dots > div').forEach(function(d,i) {{
            d.style.background = i===currentScene ? 'rgba(255,255,255,0.8)' : 'rgba(255,255,255,0.3)';
          }});

          // Sync active narration: pause scenes not at current position, play matching ones
          allAudio.forEach(function(a) {{
            var aStart = parseFloat(a.getAttribute('data-start') || '0');
            var aEnd = aStart + parseFloat(a.getAttribute('data-duration') || '0');
            if (t >= aStart && t <= aEnd + 0.5) {{
              // This audio should be active
              if (a.paused && !tl.paused()) {{
                a.currentTime = Math.max(0, t - aStart);
                var p = a.play(); if (p && p.then) p.catch(function(){{}});
              }}
              // Drift correction
              if (!a.paused && Math.abs(a.currentTime - (t - aStart)) > 0.15)
                a.currentTime = t - aStart;
            }} else {{
              // This audio is outside its active window
              if (!a.paused) a.pause();
            }}
          }});

          if (timeEl) {{
            var mt=Math.floor(t/60),st=Math.floor(t%60);
            timeEl.textContent=(mt<10?'0':'')+mt+':'+(st<10?'0':'')+st+' / '+
              (Math.floor(DURATION/60)<10?'0':'')+Math.floor(DURATION/60)+':'+
              (Math.floor(DURATION%60)<10?'0':'')+Math.floor(DURATION%60);
          }}
        }}

        bar.addEventListener('click', function(e) {{ seekBoth(e.offsetX/bar.offsetWidth*DURATION); updatePreview(); }});
        bar.addEventListener('mousedown', function(e) {{
          var wp = !tl.paused(); pauseBoth();
          function mv(ev) {{ seekBoth(Math.max(0,Math.min(1,(ev.clientX-bar.getBoundingClientRect().left)/bar.offsetWidth))*DURATION); updatePreview(); }}
          function up() {{ document.removeEventListener('mousemove',mv); document.removeEventListener('mouseup',up); if (wp) playBoth(); }}
          document.addEventListener('mousemove',mv); document.addEventListener('mouseup',up); mv(e);
        }});

        document.addEventListener('keydown', function(e) {{
          if (e.key==='ArrowRight' && currentScene<SCENES.length-1)
            {{ currentScene++; seekBoth(SCENES[currentScene]); updatePreview(); }}
          else if (e.key==='ArrowLeft' && currentScene>0)
            {{ currentScene--; seekBoth(SCENES[currentScene]); updatePreview(); }}
          else if (e.key===' ') {{ e.preventDefault(); tl.paused() ? playBoth() : pauseBoth(); }}
        }});

        gsap.ticker.add(updatePreview);
      }})();
"""


def main():
    parser = argparse.ArgumentParser(description="Generate preview HTML from render HTML")
    parser.add_argument("--input", required=True, help="Path to render HTML")
    parser.add_argument("--output", required=True, help="Path to preview HTML output")
    parser.add_argument("--scenes", required=True, help="Comma-separated scene timestamps in seconds")
    parser.add_argument("--duration", type=float, required=True, help="Total duration in seconds")
    args = parser.parse_args()

    with open(args.input, 'r') as f:
        html = f.read()

    # Inject preview CSS before </style>
    html = html.replace('</style>', PREVIEW_CSS + '</style>', 1)
    if '</style>' not in html:
        print("Warning: no </style> found — inline style may not be present")

    # Inject preview HTML before </body>
    html = html.replace('</body>', PREVIEW_HTML + '\n</body>', 1)
    if '</body>' not in html:
        print("Error: no </body> found in render HTML")
        sys.exit(1)

    # Inject preview JS before the LAST })(); (timeline closure)
    # Find the last `})();` which closes the GSAP timeline IIFE
    matches = list(re.finditer(r'\}\)\s*\(\s*\)\s*;', html))
    if matches:
        last_match = matches[-1]
        html = html[:last_match.start()] + PREVIEW_JS_TEMPLATE.format(
            scenes=args.scenes,
            duration=int(args.duration)
        ) + html[last_match.end():]
    else:
        # Fallback: inject before </script>
        html = html.replace('</script>', PREVIEW_JS_TEMPLATE.format(
            scenes=args.scenes, duration=int(args.duration)
        ) + '\n</script>', 1)
        print("Warning: no })(); found, injected before </script>")

    with open(args.output, 'w') as f:
        f.write(html)

    print(f"Preview HTML: {args.output}")
    print(f"  Scenes: {args.scenes}")
    print(f"  Duration: {args.duration}s")


if __name__ == "__main__":
    main()
