/* === JitterFree — Export ===
   Three formats: WebM (MediaRecorder), GIF (gif.js), PNG sequence (jszip).
   All driven by a virtual clock + the same renderComposition() used by the preview.
*/

const FORMATS = [
  { id: "mp4",  name: "MP4",   desc: "H.264 video" },
  { id: "mov",  name: "MOV",   desc: "QuickTime" },
  { id: "webm", name: "WebM",  desc: "Video, transparent" },
  { id: "gif",  name: "GIF",   desc: "Animated, looping" },
  { id: "png",  name: "PNG",   desc: "Frame sequence" },
];

// Probe browser support once.
const MP4_MIME_CANDIDATES = [
  "video/mp4;codecs=avc1.42E01E",
  "video/mp4;codecs=avc1",
  "video/mp4;codecs=h264",
  "video/mp4",
];
function supportedMp4Mime() {
  if (typeof MediaRecorder === "undefined") return null;
  for (const m of MP4_MIME_CANDIDATES) {
    try { if (MediaRecorder.isTypeSupported(m)) return m; } catch (e) {}
  }
  return null;
}

const FPS_OPTIONS = [24, 30, 60];
const RES_OPTIONS = [
  { id: "720",  label: "720p",     w: 1152, h: 720 },
  { id: "1080", label: "1080p",    w: 1920, h: 1200 },
  { id: "src",  label: "Original", w: window.CANVAS_W, h: window.CANVAS_H },
];

/* Load a script tag once and resolve when ready. */
function loadScript(src, integrity) {
  return new Promise((resolve, reject) => {
    if (document.querySelector(`script[data-export-src="${src}"]`)) return resolve();
    const s = document.createElement("script");
    s.src = src;
    s.async = true;
    s.dataset.exportSrc = src;
    s.onload = () => resolve();
    s.onerror = () => reject(new Error("failed: " + src));
    document.head.appendChild(s);
  });
}

/* Render one frame of the composition into a target canvas at given virtual time. */
function renderFrame(targetCanvas, layers, t) {
  const ctx = targetCanvas.getContext("2d");
  window.renderComposition(ctx, layers, t, {
    width: targetCanvas.width, height: targetCanvas.height, bg: "#ffffff",
  });
}

function downloadBlob(blob, filename) {
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  setTimeout(() => URL.revokeObjectURL(url), 2000);
}

/* ===== MediaRecorder-based video export (WebM or MP4) ===== */
async function exportRecorded({ layers, totalDur, fps, resolution, onProgress, signal, container }) {
  const cv = document.createElement("canvas");
  cv.width = resolution.w; cv.height = resolution.h;
  const stream = cv.captureStream(fps);

  let mime;
  if (container === "mp4" || container === "mov") {
    mime = supportedMp4Mime();
    if (!mime) {
      throw new Error("Your browser doesn't support MP4 recording. Try Chrome 126+, Edge, or Safari \u2014 or use the WebM option.");
    }
  } else {
    mime = MediaRecorder.isTypeSupported("video/webm;codecs=vp9")
      ? "video/webm;codecs=vp9"
      : MediaRecorder.isTypeSupported("video/webm;codecs=vp8")
      ? "video/webm;codecs=vp8"
      : "video/webm";
  }
  const chunks = [];
  const recorder = new MediaRecorder(stream, { mimeType: mime, videoBitsPerSecond: 8_000_000 });
  recorder.ondataavailable = (e) => { if (e.data.size) chunks.push(e.data); };
  const stopped = new Promise((res) => recorder.onstop = res);
  recorder.start();

  const totalFrames = Math.ceil(totalDur * fps);
  const frameDur = 1 / fps;
  const startWall = performance.now();
  for (let i = 0; i < totalFrames; i++) {
    if (signal?.aborted) break;
    const t = i * frameDur;
    renderFrame(cv, layers, t);
    onProgress?.(i / totalFrames);
    const target = startWall + i * (1000 / fps);
    const wait = target - performance.now();
    if (wait > 0) await new Promise((r) => setTimeout(r, wait));
  }
  // Hold the last frame briefly so MediaRecorder commits it.
  await new Promise((r) => setTimeout(r, 1000 / fps + 80));
  recorder.stop();
  await stopped;
  // For .mov, reuse the MP4 container (QuickTime accepts ISO-BMFF in .mov files).
  const outType = container === "mov" ? "video/quicktime" : (container === "mp4" ? "video/mp4" : "video/webm");
  const blob = new Blob(chunks, { type: outType });
  onProgress?.(1);
  return blob;
}

/* ===== GIF via gif.js ===== */
async function exportGIF({ layers, totalDur, fps, resolution, onProgress, signal }) {
  const GIF_CDN    = "https://cdn.jsdelivr.net/npm/gif.js@0.2.0/dist/gif.js";
  const WORKER_CDN = "https://cdn.jsdelivr.net/npm/gif.js@0.2.0/dist/gif.worker.js";
  await loadScript(GIF_CDN);
  // Fetch worker script as blob URL so cross-origin worker creation works.
  let workerBlobUrl = WORKER_CDN;
  try {
    const r = await fetch(WORKER_CDN);
    const txt = await r.text();
    workerBlobUrl = URL.createObjectURL(new Blob([txt], { type: "application/javascript" }));
  } catch (e) { /* fallback to direct URL */ }

  const cv = document.createElement("canvas");
  cv.width = resolution.w; cv.height = resolution.h;

  const gif = new window.GIF({
    workers: 2,
    quality: 8,
    width: cv.width,
    height: cv.height,
    workerScript: workerBlobUrl,
  });

  const totalFrames = Math.ceil(totalDur * fps);
  const frameDur = 1 / fps;
  for (let i = 0; i < totalFrames; i++) {
    if (signal?.aborted) break;
    const t = i * frameDur;
    renderFrame(cv, layers, t);
    gif.addFrame(cv, { copy: true, delay: 1000 / fps });
    onProgress?.(0.5 * (i / totalFrames));
    if (i % 8 === 0) await new Promise((r) => setTimeout(r, 0));
  }

  return new Promise((resolve, reject) => {
    gif.on("progress", (p) => onProgress?.(0.5 + 0.5 * p));
    gif.on("finished", (blob) => { onProgress?.(1); resolve(blob); });
    if (signal) signal.addEventListener("abort", () => { try { gif.abort(); } catch(e){} reject(new Error("aborted")); });
    gif.render();
  });
}

/* ===== PNG sequence via JSZip ===== */
async function exportPNG({ layers, totalDur, fps, resolution, onProgress, signal }) {
  await loadScript("https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js");
  const zip = new window.JSZip();
  const cv = document.createElement("canvas");
  cv.width = resolution.w; cv.height = resolution.h;
  const totalFrames = Math.ceil(totalDur * fps);
  const frameDur = 1 / fps;
  for (let i = 0; i < totalFrames; i++) {
    if (signal?.aborted) break;
    const t = i * frameDur;
    renderFrame(cv, layers, t);
    const blob = await new Promise((r) => cv.toBlob(r, "image/png"));
    if (!blob) continue;
    zip.file(`frame_${String(i).padStart(4, "0")}.png`, blob);
    onProgress?.(0.5 * (i / totalFrames));
    if (i % 6 === 0) await new Promise((r) => setTimeout(r, 0));
  }
  const zipBlob = await zip.generateAsync({ type: "blob" }, (m) => onProgress?.(0.5 + 0.5 * (m.percent / 100)));
  onProgress?.(1);
  return zipBlob;
}

/* ===== Modal UI ===== */
const ExportModal = ({ open, onClose, comp, totalDur }) => {
  const [format, setFormat] = useState(() => (typeof MediaRecorder !== "undefined" && supportedMp4Mime()) ? "mp4" : "webm");
  const [fps, setFps] = useState(30);
  const [resId, setResId] = useState("1080");
  const [progress, setProgress] = useState(0);
  const [exporting, setExporting] = useState(false);
  const [error, setError] = useState(null);
  const abortRef = useRef(null);
  const mp4Supported = useMemo(() => !!supportedMp4Mime(), []);

  if (!open) return null;

  const resolution = RES_OPTIONS.find((r) => r.id === resId);
  const formatUnsupported = (format === "mp4" || format === "mov") && !mp4Supported;

  const handleExport = async () => {
    setError(null); setProgress(0); setExporting(true);
    const ctrl = new AbortController();
    abortRef.current = ctrl;
    try {
      const args = {
        layers: comp.layers, totalDur,
        fps, resolution,
        onProgress: setProgress,
        signal: ctrl.signal,
      };
      let blob, ext;
      if (format === "mp4")  { blob = await exportRecorded({ ...args, container: "mp4" });  ext = "mp4"; }
      else if (format === "mov")  { blob = await exportRecorded({ ...args, container: "mov" });  ext = "mov"; }
      else if (format === "webm") { blob = await exportRecorded({ ...args, container: "webm" }); ext = "webm"; }
      else if (format === "gif")  { blob = await exportGIF(args);  ext = "gif"; }
      else                         { blob = await exportPNG(args);  ext = "zip"; }
      if (!ctrl.signal.aborted && blob) {
        const fname = `animation_${Date.now()}.${ext}`;
        downloadBlob(blob, fname);
        setExporting(false);
        onClose();
      } else {
        setExporting(false);
      }
    } catch (e) {
      console.error(e);
      setError(e.message || String(e));
      setExporting(false);
    }
  };

  const cancel = () => {
    abortRef.current?.abort();
    setExporting(false);
    onClose();
  };

  return (
    <div className="modal-backdrop" onClick={(e) => { if (e.target === e.currentTarget && !exporting) onClose(); }}>
      <div className="modal" role="dialog">
        <div className="modal-header">
          <div className="modal-title">Export animation</div>
          <button className="icon-btn" onClick={() => !exporting && onClose()}><window.Icon name="x" size={14}/></button>
        </div>
        <div className="modal-body">
          <div className="opt-row">
            <div className="opt-label">Format</div>
            <div className="format-grid">
              {FORMATS.map((f) => {
                const unavailable = (f.id === "mp4" || f.id === "mov") && !mp4Supported;
                return (
                  <button key={f.id}
                          className={`format-card${format === f.id ? " selected" : ""}${unavailable ? " disabled" : ""}`}
                          onClick={() => setFormat(f.id)}
                          disabled={exporting}
                          title={unavailable ? "Not supported by this browser \u2014 try Chrome 126+ or Safari" : undefined}>
                    <span className="format-name">{f.name}</span>
                    <span className="format-desc">{unavailable ? "Not supported here" : f.desc}</span>
                  </button>
                );
              })}
            </div>
          </div>

          <div className="opt-row">
            <div className="opt-label">Resolution</div>
            <div className="opt-segments">
              {RES_OPTIONS.map((r) => (
                <button key={r.id} className={`opt-segment${resId === r.id ? " selected" : ""}`}
                        onClick={() => setResId(r.id)} disabled={exporting}>
                  {r.label} <span style={{ color: "var(--fg-4)", fontWeight: 400, marginLeft: 4 }}>{r.w}×{r.h}</span>
                </button>
              ))}
            </div>
          </div>

          <div className="opt-row">
            <div className="opt-label">Frame rate</div>
            <div className="opt-segments">
              {FPS_OPTIONS.map((n) => (
                <button key={n} className={`opt-segment${fps === n ? " selected" : ""}`}
                        onClick={() => setFps(n)} disabled={exporting}>{n} fps</button>
              ))}
            </div>
          </div>

          <div className="opt-row" style={{ paddingTop: 4 }}>
            <div style={{ display: "flex", justifyContent: "space-between", fontSize: 11, color: "var(--fg-3)" }}>
              <span>Duration {totalDur.toFixed(1)}s · {Math.ceil(totalDur * fps)} frames</span>
              <span>Output {resolution.w} × {resolution.h}</span>
            </div>
          </div>

          {(exporting || progress > 0) && (
            <div>
              <div className="progress-track"><div className="progress-fill" style={{ width: `${Math.round(progress * 100)}%` }}/></div>
              <div className="progress-meta">
                <span>{exporting ? "Rendering…" : "Ready"}</span>
                <span>{Math.round(progress * 100)}%</span>
              </div>
            </div>
          )}
          {error && (
            <div style={{ background: "rgba(220, 38, 38, 0.08)", color: "#b91c1c", padding: 10, borderRadius: 6, fontSize: 12 }}>
              {error}
            </div>
          )}
        </div>
        <div className="modal-footer">
          {exporting
            ? <button className="btn" onClick={cancel}>Cancel</button>
            : <>
                <button className="btn ghost" onClick={onClose}>Close</button>
                <button className="btn primary" onClick={handleExport} disabled={!comp.layers.length || formatUnsupported}>
                  <window.Icon name="download" size={12}/> Export
                </button>
              </>}
        </div>
      </div>
    </div>
  );
};

window.ExportModal = ExportModal;
