// shotseq.com — main app
// React + Babel. Reads i18n globals injected by Astro and encoder.js.

const { useState, useEffect, useRef, useMemo, useCallback, Fragment } = React;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "dark": false,
  "accent": "#111111",
  "showSettings": true
}/*EDITMODE-END*/;

// ── helpers ────────────────────────────────────────────────────────────────
const fmtTime = (s) => {
  if (!isFinite(s)) return "00:00";
  const m = Math.floor(s / 60);
  const sec = Math.floor(s % 60);
  return `${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
};
const fmtBytes = (n) => {
  if (n < 1024) return `${n} B`;
  if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
  return `${(n / 1024 / 1024).toFixed(1)} MB`;
};

const ASPECTS = {
  "16:9": [16, 9],
  "9:16": [9, 16],
  "1:1":  [1, 1],
  "4:5":  [4, 5],
  "3:2":  [3, 2],
};

const RESOLUTIONS = {
  "720p":  720,
  "1080p": 1080,
  "1440p": 1440,
  "2160p": 2160,
};

function dimsFor(aspect, res) {
  const [aw, ah] = ASPECTS[aspect];
  // res = height for landscape, longer-side for portrait/square
  const shortSide = res;
  // We treat res as the short side for portrait, height for landscape, side for square.
  let w, h;
  if (aw >= ah) {
    h = shortSide;
    w = Math.round((h * aw) / ah);
  } else {
    w = shortSide;
    h = Math.round((w * ah) / aw);
  }
  // Round to even (codec requirement)
  w = w + (w % 2);
  h = h + (h % 2);
  return [w, h];
}

const ACCENT_OPTIONS = ["#111111", "#D43A2F", "#1F5BFF", "#137A4A", "#7A3CC4"];

// ── Demo images ──────────────────────────────────────────────────────────
// Curated free photos from Unsplash. Loaded with CORS so we can canvas-encode.
// Photographer credits render under the filmstrip.
const DEMO_PHOTOS = [
  {
    id: "jUCQRQeRs3k",
    by: "Willian Justen de Vasconcellos",
    handle: "willianjusten",
    url: "https://images.unsplash.com/photo-1521080755838-d2311117f767?w=1920&q=80&auto=format&fit=crop",
  },
  {
    id: "wVjd0eWNqI8",
    by: "Paul Earle",
    handle: "paulearlephotography",
    url: "https://images.unsplash.com/photo-1483354483454-4cd359948304?w=1920&q=80&auto=format&fit=crop",
  },
  {
    id: "hXXRLtTxXCU",
    by: "Brigitta Schneiter",
    handle: "brisch27",
    url: "https://images.unsplash.com/photo-1484902945377-bd2a38e625cd?w=1920&q=80&auto=format&fit=crop",
  },
  {
    id: "6YmzwamGzCg",
    by: "Mads Schmidt Rasmussen",
    handle: "mvds",
    url: "https://images.unsplash.com/photo-1519120944692-1a8d8cfc107f?w=1920&q=80&auto=format&fit=crop",
  },
];

// Fallback palettes used only when a photo fails to load (offline, blocked).
const FALLBACK_PALETTES = [
  [[0, "#f4d6a6"], [0.55, "#d6855a"], [1, "#4a1f1c"]],
  [[0, "#1a3a5a"], [0.6, "#3e6b8c"], [1, "#a9c4d6"]],
  [[0, "#1a1a1a"], [0.55, "#3a3633"], [1, "#a3998c"]],
  [[0, "#2a0d12"], [0.5, "#7a2530"], [1, "#e6a48a"]],
];
function makeFallbackImage(idx, w = 1600, h = 900) {
  const cv = document.createElement("canvas");
  cv.width = w; cv.height = h;
  const ctx = cv.getContext("2d");
  const stops = FALLBACK_PALETTES[idx % FALLBACK_PALETTES.length];
  const g = ctx.createLinearGradient(0, 0, 0, h);
  stops.forEach(([p, c]) => g.addColorStop(p, c));
  ctx.fillStyle = g; ctx.fillRect(0, 0, w, h);
  return cv;
}

function loadCrossOriginImage(url) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.crossOrigin = "anonymous";
    img.referrerPolicy = "no-referrer-when-downgrade";
    img.onload = () => resolve(img);
    img.onerror = () => reject(new Error("load_failed"));
    img.src = url;
  });
}

function buildDemoImages(perImageDefault = 2) {
  return DEMO_PHOTOS.map((photo, i) =>
    loadCrossOriginImage(photo.url)
      .then((img) => ({
        id: "demo-" + photo.id,
        name: `unsplash-${photo.id}.jpg`,
        file: null,
        img,
        url: photo.url,
        duration: perImageDefault,
        isDemo: true,
        credit: photo,
      }))
      .catch(() => new Promise((resolve) => {
        // Fallback: offline gradient placeholder, keeps the credit reference.
        const cv = makeFallbackImage(i);
        cv.toBlob((blob) => {
          const blobUrl = URL.createObjectURL(blob);
          const im = new Image();
          im.onload = () => resolve({
            id: "demo-fb-" + i,
            name: `demo-0${i + 1}.png`,
            file: null,
            img: im,
            url: blobUrl,
            duration: perImageDefault,
            isDemo: true,
            credit: { ...photo, offline: true },
          });
          im.src = blobUrl;
        }, "image/png");
      }))
  );
}

// ── load an uploaded File into an HTMLImageElement ─────────────────────────
function loadImageFile(file) {
  return new Promise((resolve, reject) => {
    const url = URL.createObjectURL(file);
    const img = new Image();
    img.onload = () => resolve({ img, url });
    img.onerror = () => {
      URL.revokeObjectURL(url);
      reject(new Error("decode"));
    };
    img.src = url;
  });
}

// ── small inline icons (stroke only, minimal) ──────────────────────────────
const Icon = {
  Play: (p) => <svg viewBox="0 0 16 16" width="14" height="14" {...p}><path d="M4 3l9 5-9 5z" fill="currentColor"/></svg>,
  Pause: (p) => <svg viewBox="0 0 16 16" width="14" height="14" {...p}><rect x="4" y="3" width="3" height="10" fill="currentColor"/><rect x="9" y="3" width="3" height="10" fill="currentColor"/></svg>,
  Plus: (p) => <svg viewBox="0 0 16 16" width="14" height="14" {...p}><path d="M8 3v10M3 8h10" stroke="currentColor" strokeWidth="1.25" fill="none" strokeLinecap="round"/></svg>,
  X: (p) => <svg viewBox="0 0 16 16" width="12" height="12" {...p}><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.25" fill="none" strokeLinecap="round"/></svg>,
  Sun: (p) => <svg viewBox="0 0 16 16" width="14" height="14" {...p}><circle cx="8" cy="8" r="3" stroke="currentColor" strokeWidth="1.1" fill="none"/><path d="M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.3 3.3l1.4 1.4M11.3 11.3l1.4 1.4M3.3 12.7l1.4-1.4M11.3 4.7l1.4-1.4" stroke="currentColor" strokeWidth="1.1" strokeLinecap="round"/></svg>,
  Moon: (p) => <svg viewBox="0 0 16 16" width="14" height="14" {...p}><path d="M13 9.5A5 5 0 1 1 6.5 3a4 4 0 0 0 6.5 6.5z" fill="currentColor"/></svg>,
  Download: (p) => <svg viewBox="0 0 16 16" width="14" height="14" {...p}><path d="M8 2v8M5 7l3 3 3-3M3 13h10" stroke="currentColor" strokeWidth="1.25" fill="none" strokeLinecap="round" strokeLinejoin="round"/></svg>,
  Drag: (p) => <svg viewBox="0 0 16 16" width="10" height="10" {...p}><circle cx="6" cy="4" r="1" fill="currentColor"/><circle cx="10" cy="4" r="1" fill="currentColor"/><circle cx="6" cy="8" r="1" fill="currentColor"/><circle cx="10" cy="8" r="1" fill="currentColor"/><circle cx="6" cy="12" r="1" fill="currentColor"/><circle cx="10" cy="12" r="1" fill="currentColor"/></svg>,
};

// ── Top bar ────────────────────────────────────────────────────────────────
function TopBar({ tr, lang, setLang, dark, setDark, hasImages, onExport, onUpload, busy }) {
  return (
    <header className="topbar">
      <div className="brand">
        <span className="brand-mark">shotseq<span className="brand-dot">.</span><span className="brand-tld">com</span></span>
        <span className="brand-tag">{tr("tagline")}</span>
      </div>
      <div className="topbar-right">
        <button className="ghost-btn" onClick={onUpload}>
          <Icon.Plus/> <span>{tr("upload")}</span>
        </button>
        <label className="lang-wrap" title={tr("lang")}>
          <select className="lang-select" value={lang} onChange={(e) => setLang(e.target.value)}>
            {LANG_ORDER.map((l) => <option key={l} value={l}>{LANG_LABEL[l]}</option>)}
          </select>
        </label>
        <button className="icon-btn" onClick={() => setDark(!dark)} title={dark ? tr("theme_light") : tr("theme_dark")}>
          {dark ? <Icon.Sun/> : <Icon.Moon/>}
        </button>
        <button
          className="primary-btn"
          disabled={!hasImages || busy}
          onClick={onExport}>
          <Icon.Download/> <span>{busy ? tr("exporting") : tr("export")}</span>
        </button>
      </div>
    </header>
  );
}

// ── Dropzone (empty state) ─────────────────────────────────────────────────
function EmptyDrop({ tr, onFiles }) {
  const [over, setOver] = useState(false);
  return (
    <div
      className={"empty " + (over ? "drag-over" : "")}
      onDragOver={(e) => { e.preventDefault(); setOver(true); }}
      onDragLeave={() => setOver(false)}
      onDrop={(e) => {
        e.preventDefault();
        setOver(false);
        const files = [...e.dataTransfer.files].filter((f) => f.type.startsWith("image/"));
        if (files.length) onFiles(files);
      }}
      onClick={() => document.getElementById("__file_in").click()}
    >
      <div className="empty-inner">
        <div className="empty-mark">
          <svg viewBox="0 0 64 64" width="56" height="56" aria-hidden="true">
            <rect x="6"  y="14" width="40" height="28" fill="none" stroke="currentColor" strokeWidth="1"/>
            <rect x="14" y="20" width="40" height="28" fill="none" stroke="currentColor" strokeWidth="1"/>
            <rect x="22" y="26" width="40" height="28" fill="none" stroke="currentColor" strokeWidth="1"/>
          </svg>
        </div>
        <div className="empty-title">{tr("drop_title")}</div>
        <div className="empty-sub">{tr("drop_sub")}</div>
      </div>
      <div className="empty-footer">{tr("footer")}</div>
    </div>
  );
}

// ── Preview canvas + scrubber ──────────────────────────────────────────────
function Preview({ tr, images, settings, currentTime, setCurrentTime, playing, setPlaying, total }) {
  const canvasRef = useRef(null);
  const wrapRef = useRef(null);
  const rafRef = useRef(0);
  const lastTickRef = useRef(0);

  // Compute display canvas size based on aspect.
  const [aw, ah] = ASPECTS[settings.aspect];
  const aspectRatio = aw / ah;

  // Render frame whenever inputs change.
  useEffect(() => {
    const c = canvasRef.current;
    if (!c) return;
    // Use a preview render size (smaller than export) for speed.
    const previewW = 1280;
    const previewH = Math.round(previewW / aspectRatio);
    if (c.width !== previewW || c.height !== previewH) {
      c.width = previewW;
      c.height = previewH;
    }
    const ctx = c.getContext("2d");
    if (images.length === 0) {
      ctx.fillStyle = settings.background;
      ctx.fillRect(0, 0, c.width, c.height);
      return;
    }
    const { segs } = window.buildSegments(images, settings.transition, settings.fadeDuration);
    window.renderFrame(ctx, c.width, c.height, segs, Math.min(currentTime, total - 0.001), settings.transition, settings.fadeDuration, settings.fit, settings.background);
  }, [images, settings, currentTime, total, aspectRatio]);

  // Playback loop.
  useEffect(() => {
    if (!playing) {
      cancelAnimationFrame(rafRef.current);
      return;
    }
    lastTickRef.current = performance.now();
    const tick = (now) => {
      const dt = (now - lastTickRef.current) / 1000;
      lastTickRef.current = now;
      setCurrentTime((t) => {
        let nt = t + dt;
        if (nt >= total) {
          setPlaying(false);
          return total;
        }
        return nt;
      });
      rafRef.current = requestAnimationFrame(tick);
    };
    rafRef.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(rafRef.current);
  }, [playing, total, setCurrentTime, setPlaying]);

  return (
    <div className="preview" ref={wrapRef}>
      <div className="preview-canvas-wrap">
        <canvas ref={canvasRef} className="preview-canvas" style={{ aspectRatio: `${aw} / ${ah}` }}/>
        {images.length === 0 && (
          <div className="preview-empty">{tr("select_image")}</div>
        )}
      </div>
      <div className="scrub">
        <button className="ghost-btn" onClick={() => setPlaying(!playing)} disabled={!images.length || total <= 0}>
          {playing ? <Icon.Pause/> : <Icon.Play/>}
        </button>
        <div className="time">{fmtTime(currentTime)} <span className="time-sep">/</span> <span className="time-total">{fmtTime(total)}</span></div>
        <input
          className="scrub-bar"
          type="range"
          min={0}
          max={Math.max(0.001, total)}
          step={0.01}
          value={Math.min(currentTime, total)}
          onChange={(e) => { setPlaying(false); setCurrentTime(parseFloat(e.target.value)); }}
        />
        <div className="dims">
          {(() => {
            const [w, h] = dimsFor(settings.aspect, RESOLUTIONS[settings.resolution]);
            return `${w}×${h}`;
          })()} · {settings.fps}{tr("fps_short")}
        </div>
      </div>
    </div>
  );
}

// ── Filmstrip ──────────────────────────────────────────────────────────────
function Filmstrip({ tr, images, setImages, selectedId, setSelectedId, onAdd, currentTime, setCurrentTime }) {
  const dragId = useRef(null);
  const [dragOverId, setDragOverId] = useState(null);

  const onDragStart = (id) => (e) => {
    dragId.current = id;
    e.dataTransfer.effectAllowed = "move";
    // dummy
    try { e.dataTransfer.setData("text/plain", id); } catch (_) {}
  };
  const onDragOver = (id) => (e) => {
    e.preventDefault();
    if (id !== dragOverId) setDragOverId(id);
  };
  const onDrop = (id) => (e) => {
    e.preventDefault();
    const from = dragId.current;
    setDragOverId(null);
    if (!from || from === id) return;
    setImages((prev) => {
      const next = [...prev];
      const fi = next.findIndex((x) => x.id === from);
      const ti = next.findIndex((x) => x.id === id);
      if (fi < 0 || ti < 0) return prev;
      const [m] = next.splice(fi, 1);
      next.splice(ti, 0, m);
      return next;
    });
  };

  const setDur = (id, v) => {
    setImages((prev) => prev.map((im) => im.id === id ? { ...im, duration: Math.max(0.1, v) } : im));
  };
  const remove = (id) => {
    setImages((prev) => prev.filter((im) => im.id !== id));
    if (selectedId === id) setSelectedId(null);
  };

  return (
    <div className="strip">
      <div className="strip-meta">
        <span className="strip-count">{tr("images_n")(images.length)}</span>
        <span className="strip-hint">
          {images.length > 0 && images.every((i) => i.isDemo) ? tr("demo_hint") : tr("empty_hint")}
        </span>
        <button className="ghost-btn small" onClick={() => {
          images.forEach((im) => { if (im.isDemo) { try { URL.revokeObjectURL(im.url); } catch(_) {} } });
          setImages([]);
          setSelectedId(null);
        }}>{tr("clear")}</button>
      </div>
      <div className="strip-row">
        {images.map((im) => (
          <div
            key={im.id}
            className={
              "thumb" +
              (selectedId === im.id ? " selected" : "") +
              (dragOverId === im.id ? " drag-over" : "")
            }
            draggable
            onDragStart={onDragStart(im.id)}
            onDragOver={onDragOver(im.id)}
            onDragLeave={() => setDragOverId((x) => (x === im.id ? null : x))}
            onDrop={onDrop(im.id)}
            onClick={() => setSelectedId(im.id)}
          >
            <img className="thumb-img" src={im.url} alt="" draggable={false}/>
            <button className="thumb-x" onClick={(e) => { e.stopPropagation(); remove(im.id); }} title={tr("remove")}>
              <Icon.X/>
            </button>
            <div className="thumb-bar" onClick={(e) => e.stopPropagation()}>
              <input
                className="thumb-dur"
                type="number"
                step="0.1"
                min="0.1"
                value={im.duration}
                onChange={(e) => setDur(im.id, parseFloat(e.target.value) || 0.1)}
              />
              <span className="thumb-dur-unit">{tr("seconds_short")}</span>
            </div>
          </div>
        ))}
        <button className="thumb add" onClick={onAdd} title={tr("add_more")}>
          <Icon.Plus/>
        </button>
      </div>
      {images.length > 0 && images.every((i) => i.isDemo) && (
        <div className="credits">
          <span className="credits-label">{tr("photos_by")}</span>
          {DEMO_PHOTOS.map((p, i) => (
            <span key={p.id} className="credits-item">
              {i > 0 && <span className="credits-sep">,</span>}
              <a
                className="credits-link"
                href={`https://unsplash.com/@${p.handle}?utm_source=shotseq&utm_medium=referral`}
                target="_blank"
                rel="noopener noreferrer"
              >{p.by}</a>
            </span>
          ))}
          <span className="credits-sep">·</span>
          <a
            className="credits-link"
            href="https://unsplash.com?utm_source=shotseq&utm_medium=referral"
            target="_blank"
            rel="noopener noreferrer"
          >{tr("on_unsplash")}</a>
        </div>
      )}
    </div>
  );
}

// ── Settings panel (right rail) ────────────────────────────────────────────
function FieldRow({ label, children }) {
  return (
    <div className="field">
      <label className="field-label">{label}</label>
      <div className="field-control">{children}</div>
    </div>
  );
}

function SegRadio({ value, options, onChange }) {
  return (
    <div className="seg">
      {options.map((o) => (
        <button
          key={o.value}
          className={"seg-opt " + (value === o.value ? "active" : "")}
          onClick={() => onChange(o.value)}
          type="button"
        >{o.label}</button>
      ))}
    </div>
  );
}

function SettingsPanel({ tr, settings, setSettings, perImageDefault, setPerImageDefault, applyDurationAll }) {
  const upd = (k) => (v) => setSettings((s) => ({ ...s, [k]: v }));
  const [w, h] = dimsFor(settings.aspect, RESOLUTIONS[settings.resolution]);

  return (
    <aside className="settings">
      <section className="sect">
        <h3 className="sect-h">{tr("output")}</h3>
        <FieldRow label={tr("aspect")}>
          <SegRadio
            value={settings.aspect}
            onChange={upd("aspect")}
            options={Object.keys(ASPECTS).map((k) => ({ value: k, label: k }))}
          />
        </FieldRow>
        <FieldRow label={tr("resolution")}>
          <SegRadio
            value={settings.resolution}
            onChange={upd("resolution")}
            options={Object.keys(RESOLUTIONS).map((k) => ({ value: k, label: k }))}
          />
        </FieldRow>
        <FieldRow label={tr("framerate")}>
          <SegRadio
            value={settings.fps}
            onChange={(v) => upd("fps")(v)}
            options={[24, 30, 60].map((n) => ({ value: n, label: String(n) }))}
          />
        </FieldRow>
        <FieldRow label={tr("format")}>
          <SegRadio
            value={settings.format}
            onChange={upd("format")}
            options={[{ value: "mp4", label: "MP4 / H.264" }, { value: "webm", label: "WebM / VP9" }]}
          />
        </FieldRow>
        <div className="dim-readout">{w} × {h}</div>
      </section>

      <section className="sect">
        <h3 className="sect-h">{tr("timing")}</h3>
        <FieldRow label={tr("per_image")}>
          <div className="row-num">
            <input
              type="number"
              step="0.1"
              min="0.1"
              className="num"
              value={perImageDefault}
              onChange={(e) => setPerImageDefault(parseFloat(e.target.value) || 0.1)}
            />
            <span className="num-unit">{tr("seconds_short")}</span>
            <button className="apply-all" onClick={() => applyDurationAll(perImageDefault)}>
              ↺
            </button>
          </div>
        </FieldRow>
        <FieldRow label={tr("transition")}>
          <SegRadio
            value={settings.transition}
            onChange={upd("transition")}
            options={[
              { value: "cut",  label: tr("transition_cut") },
              { value: "fade", label: tr("transition_fade") },
            ]}
          />
        </FieldRow>
        {settings.transition === "fade" && (
          <FieldRow label={tr("fade_length")}>
            <div className="row-num">
              <input
                type="number"
                step="0.05"
                min="0.05"
                max="2"
                className="num"
                value={settings.fadeDuration}
                onChange={(e) => upd("fadeDuration")(parseFloat(e.target.value) || 0.05)}
              />
              <span className="num-unit">{tr("seconds_short")}</span>
            </div>
          </FieldRow>
        )}
      </section>

      <section className="sect">
        <h3 className="sect-h">{tr("preview")}</h3>
        <FieldRow label={tr("fit")}>
          <SegRadio
            value={settings.fit}
            onChange={upd("fit")}
            options={[
              { value: "contain", label: tr("fit_contain") },
              { value: "cover",   label: tr("fit_cover")   },
            ]}
          />
        </FieldRow>
        <FieldRow label={tr("background")}>
          <div className="bg-swatches">
            {["#000000", "#ffffff", "#0a0a0a", "#f6f4ef", "#1a2433"].map((c) => (
              <button
                key={c}
                className={"bg-sw " + (settings.background === c ? "active" : "")}
                style={{ background: c }}
                onClick={() => upd("background")(c)}
              />
            ))}
            <input
              type="color"
              value={settings.background}
              onChange={(e) => upd("background")(e.target.value)}
              className="bg-color"
              title={tr("background")}
            />
          </div>
        </FieldRow>
      </section>
    </aside>
  );
}

// ── Export overlay ─────────────────────────────────────────────────────────
function ExportOverlay({ tr, state, onClose, onAbort }) {
  if (!state) return null;
  return (
    <div className="overlay">
      <div className="overlay-card">
        {state.phase === "encoding" && (
          <Fragment>
            <div className="ov-title">{tr("exporting")}</div>
            <div className="ov-progress-track">
              <div className="ov-progress-fill" style={{ width: `${state.pct}%` }}/>
            </div>
            <div className="ov-meta">
              <span>{tr("encoding_progress")(state.pct)}</span>
              <span>{state.frame}/{state.total}</span>
            </div>
            <button className="ghost-btn" onClick={onAbort}>{tr("remove")}</button>
          </Fragment>
        )}
        {state.phase === "done" && (
          <Fragment>
            <div className="ov-title">{tr("download")}</div>
            <div className="ov-sub">
              {state.meta.filename} · {fmtBytes(state.meta.size)} · {tr("encoded_in")((state.meta.ms / 1000).toFixed(1))}
            </div>
            <video className="ov-video" src={state.url} controls autoPlay muted loop/>
            <div className="ov-actions">
              <a className="primary-btn" href={state.url} download={state.meta.filename}>
                <Icon.Download/> <span>{tr("download")}</span>
              </a>
              <button className="ghost-btn" onClick={onClose}>{tr("new_export")}</button>
            </div>
          </Fragment>
        )}
        {state.phase === "error" && (
          <Fragment>
            <div className="ov-title">{state.message}</div>
            <div className="ov-actions">
              <button className="ghost-btn" onClick={onClose}>OK</button>
            </div>
          </Fragment>
        )}
      </div>
    </div>
  );
}

// ── App ────────────────────────────────────────────────────────────────────
function App() {
  const [t, setTweak] = window.useTweaks(TWEAK_DEFAULTS);
  const [lang, setLangRaw] = useState(() => {
    if (window.__SHOTSEQ_LANG__ && I18N[window.__SHOTSEQ_LANG__]) return window.__SHOTSEQ_LANG__;
    try {
      const saved = localStorage.getItem("shotseq.lang");
      if (saved && I18N[saved]) return saved;
    } catch (_) {}
    const nav = (navigator.language || "en").toLowerCase();
    if (nav.startsWith("zh-tw") || nav.startsWith("zh-hk") || nav.includes("hant")) return "zh-TW";
    if (nav.startsWith("zh")) return "zh-CN";
    if (nav.startsWith("ja")) return "ja";
    if (nav.startsWith("es")) return "es";
    return "en";
  });
  const setLang = useCallback((v) => {
    try { localStorage.setItem("shotseq.lang", v); } catch (_) {}
    const target = window.__SHOTSEQ_LOCALE_URLS__ && window.__SHOTSEQ_LOCALE_URLS__[v];
    if (target && target !== window.location.pathname) {
      window.location.href = target;
      return;
    }
    setLangRaw(v);
  }, []);
  const tr = useCallback((key) => {
    const d = I18N[lang] || I18N.en;
    return d[key] !== undefined ? d[key] : (I18N.en[key] !== undefined ? I18N.en[key] : key);
  }, [lang]);

  const [images, setImages] = useState([]);
  const [demoLoaded, setDemoLoaded] = useState(false);
  const [selectedId, setSelectedId] = useState(null);
  const [perImageDefault, setPerImageDefault] = useState(2);
  const [settings, setSettings] = useState({
    aspect: "16:9",
    resolution: "1080p",
    fps: 30,
    format: "mp4",
    transition: "fade",
    fadeDuration: 0.5,
    fit: "contain",
    background: "#000000",
  });
  const [currentTime, setCurrentTime] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [exportState, setExportState] = useState(null);
  const abortRef = useRef(null);

  // Bootstrap demo images so the editor isn't an empty screen on first load.
  useEffect(() => {
    if (demoLoaded) return;
    let cancelled = false;
    (async () => {
      const items = await Promise.all(buildDemoImages(perImageDefault));
      if (cancelled) return;
      setImages(items);
      setSelectedId(items[0]?.id ?? null);
      setDemoLoaded(true);
    })();
    return () => { cancelled = true; };
  }, []); // eslint-disable-line

  // Theme + accent
  useEffect(() => {
    document.documentElement.dataset.theme = t.dark ? "dark" : "light";
    document.documentElement.style.setProperty("--accent", t.accent);
  }, [t.dark, t.accent]);

  // total duration
  const total = useMemo(() => {
    if (!images.length) return 0;
    const { total } = window.buildSegments(images, settings.transition, settings.fadeDuration);
    return total;
  }, [images, settings.transition, settings.fadeDuration]);

  // clamp currentTime
  useEffect(() => {
    if (currentTime > total) setCurrentTime(total);
  }, [total]); // eslint-disable-line

  // File ingestion. When the current set is demos, replace it.
  const ingestFiles = useCallback(async (files) => {
    const news = [];
    for (const f of files) {
      try {
        const { img, url } = await loadImageFile(f);
        news.push({
          id: Math.random().toString(36).slice(2),
          name: f.name,
          file: f,
          img,
          url,
          duration: perImageDefault,
          isDemo: false,
        });
      } catch (_) {
        // ignore
      }
    }
    if (!news.length) return;
    setImages((prev) => {
      // If everything is a demo, replace; otherwise append.
      const allDemo = prev.length > 0 && prev.every((p) => p.isDemo);
      const base = allDemo ? [] : prev;
      // Revoke demo blob URLs we're dropping.
      if (allDemo) prev.forEach((p) => { try { URL.revokeObjectURL(p.url); } catch (_) {} });
      const next = [...base, ...news];
      setSelectedId((cur) => {
        if (allDemo) return next[0].id;
        return cur ?? next[0].id;
      });
      return next;
    });
  }, [perImageDefault]);

  const onFileInput = (e) => {
    const files = [...(e.target.files || [])].filter((f) => f.type.startsWith("image/"));
    e.target.value = "";
    if (files.length) ingestFiles(files);
  };

  // Drag-anywhere when images exist
  useEffect(() => {
    const onOver = (e) => { if (e.dataTransfer?.types?.includes("Files")) e.preventDefault(); };
    const onDrop = (e) => {
      if (!e.dataTransfer?.files?.length) return;
      const files = [...e.dataTransfer.files].filter((f) => f.type.startsWith("image/"));
      if (files.length) { e.preventDefault(); ingestFiles(files); }
    };
    window.addEventListener("dragover", onOver);
    window.addEventListener("drop", onDrop);
    return () => {
      window.removeEventListener("dragover", onOver);
      window.removeEventListener("drop", onDrop);
    };
  }, [ingestFiles]);

  // Apply default duration to all images
  const applyDurationAll = (v) => setImages((prev) => prev.map((im) => ({ ...im, duration: v })));

  // Export
  const doExport = async () => {
    if (!images.length) return;
    const [w, h] = dimsFor(settings.aspect, RESOLUTIONS[settings.resolution]);
    const ac = new AbortController();
    abortRef.current = ac;
    setExportState({ phase: "encoding", pct: 0, frame: 0, total: 0 });
    try {
      const result = await window.shotseqEncode({
        images, width: w, height: h, fps: settings.fps,
        transition: settings.transition,
        fadeDuration: settings.fadeDuration,
        fit: settings.fit,
        background: settings.background,
        format: settings.format,
        signal: ac.signal,
        onProgress: (frame, total) => {
          const pct = Math.round((frame / total) * 100);
          setExportState((s) => s && s.phase === "encoding" ? { ...s, pct, frame, total } : s);
        },
      });
      const url = URL.createObjectURL(result.blob);
      const filename = `shotseq-${Date.now()}.${result.ext}`;
      setExportState({
        phase: "done",
        url,
        meta: { filename, size: result.blob.size, ms: result.durationMs },
      });
    } catch (err) {
      if (err.code === "ABORTED") {
        setExportState(null);
        return;
      }
      const msg = err.code === "NO_CODEC" ? tr("no_codec") : tr("error_encode");
      setExportState({ phase: "error", message: msg });
      console.error("encode error:", err);
    }
  };
  const abortExport = () => { abortRef.current?.abort(); };

  const showSettings = t.showSettings && images.length > 0;

  return (
    <div className="app-root">
    <div className="app">
      <input id="__file_in" type="file" multiple accept="image/*" hidden onChange={onFileInput}/>

      <TopBar
        tr={tr}
        lang={lang} setLang={setLang}
        dark={t.dark} setDark={(v) => setTweak("dark", v)}
        hasImages={images.length > 0}
        onExport={doExport}
        onUpload={() => document.getElementById("__file_in").click()}
        busy={exportState?.phase === "encoding"}
      />

      <main className={"main " + (showSettings ? "with-settings" : "")}>
        {images.length === 0 ? (
          <EmptyDrop tr={tr} onFiles={ingestFiles}/>
        ) : (
          <Preview
            tr={tr}
            images={images}
            settings={settings}
            currentTime={currentTime}
            setCurrentTime={setCurrentTime}
            playing={playing}
            setPlaying={setPlaying}
            total={total}
          />
        )}
        {showSettings && (
          <SettingsPanel
            tr={tr}
            settings={settings}
            setSettings={setSettings}
            perImageDefault={perImageDefault}
            setPerImageDefault={setPerImageDefault}
            applyDurationAll={applyDurationAll}
          />
        )}
      </main>

      {images.length > 0 && (
        <Filmstrip
          tr={tr}
          images={images}
          setImages={setImages}
          selectedId={selectedId}
          setSelectedId={(id) => {
            setSelectedId(id);
            // jump preview to that image's mid-time
            const { segs } = window.buildSegments(images, settings.transition, settings.fadeDuration);
            const seg = segs.find((s) => s.img === images.find((i) => i.id === id)?.img);
            if (seg) { setPlaying(false); setCurrentTime((seg.start + seg.end) / 2); }
          }}
          onAdd={() => document.getElementById("__file_in").click()}
          currentTime={currentTime}
          setCurrentTime={setCurrentTime}
        />
      )}

      <ExportOverlay
        tr={tr}
        state={exportState}
        onClose={() => {
          if (exportState?.url) URL.revokeObjectURL(exportState.url);
          setExportState(null);
        }}
        onAbort={abortExport}
      />

      <window.TweaksPanel title="Tweaks">        <window.TweakSection label="Theme"/>
        <window.TweakToggle label={tr("theme_dark")} value={t.dark} onChange={(v) => setTweak("dark", v)}/>
        <window.TweakColor
          label="Accent"
          value={t.accent}
          options={ACCENT_OPTIONS}
          onChange={(v) => setTweak("accent", v)}
        />
        <window.TweakSection label="Layout"/>
        <window.TweakToggle
          label="Show settings panel"
          value={t.showSettings}
          onChange={(v) => setTweak("showSettings", v)}
        />
      </window.TweaksPanel>
    </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App/>);
