// Label author mode (content-pipeline §11.5): scrub a pool clip, drag a seed box
// on a subject, run the classical tracker to propagate a keyframed track, author
// the LEFT detail tiers + RIGHT emotion tiers, and save the entry to the manifest.
// Geometry comes from the tracker; SEMANTICS (keys, strings, tiers) are hand-typed.
const $ = (id) => document.getElementById(id);
const vid = $("vid"), draw = $("draw");
const SVGNS = "http://www.w3.org/2000/svg";

let clips = [];
let currentClip = null;
let seedBox = null;            // [x,y,w,h] normalized, from a drag
let pendingTrack = null;       // {track, appear, disappear} from the tracker (or static)
let affectPoint = null;        // [x,y] normalized, from a shift-click
let annotations = [];          // authored annotation dicts (carry _tiers)
let affect = [];               // authored affect dicts (carry _tiers)

function svg(tag, attrs, parent) {
  const el = document.createElementNS(SVGNS, tag);
  for (const k in attrs) el.setAttribute(k, attrs[k]);
  if (parent) parent.appendChild(el);
  return el;
}
const mono = (n) => Number(n).toFixed(3);

async function loadClips() {
  clips = (await (await fetch("/api/clips")).json()).clips || [];
  const sel = $("clip");
  sel.innerHTML = "";
  for (const c of clips) {
    const o = document.createElement("option");
    o.value = c.id; o.textContent = c.id;
    sel.appendChild(o);
  }
  if (clips.length) selectClip(clips[0].id);
}

function selectClip(id) {
  currentClip = clips.find((c) => c.id === id) || null;
  if (!currentClip) return;
  $("clip-meta").textContent = `${currentClip.title} — ${currentClip.license}`;
  vid.src = "/media/" + currentClip.base_file;
  vid.currentTime = 0;
  annotations = []; affect = []; seedBox = null; pendingTrack = null; affectPoint = null;
  renderLists(); renderDrawLayer();
  $("seed-readout").textContent = "no box — drag on the stage";
  $("track-readout").textContent = "";
  $("affect-readout").textContent = "no point — shift-click the stage";
}

// --- scrub ---
function seekT() { return vid.duration ? vid.currentTime / vid.duration : 0; }
$("play").addEventListener("click", () => { vid.paused ? vid.play() : vid.pause(); });
$("seek").addEventListener("input", () => {
  if (vid.duration) vid.currentTime = (+$("seek").value / 1000) * vid.duration;
});
vid.addEventListener("timeupdate", () => {
  if (!vid.duration) return;
  $("seek").value = String(Math.round(seekT() * 1000));
  $("time").textContent = seekT().toFixed(3);
});

// --- draw a seed box / place an affect point on the stage ---
function evtNorm(e) {
  const r = draw.getBoundingClientRect();
  return [(e.clientX - r.left) / r.width, (e.clientY - r.top) / r.height];
}
let dragStart = null;
draw.addEventListener("mousedown", (e) => {
  if (e.shiftKey) {                       // shift-click = affect anchor
    affectPoint = evtNorm(e).map((v) => Math.min(Math.max(v, 0), 1));
    $("affect-readout").textContent = `affect @ ${affectPoint.map(mono).join(", ")}`;
    $("add-aff").disabled = false;
    renderDrawLayer();
    return;
  }
  dragStart = evtNorm(e);
});
draw.addEventListener("mousemove", (e) => {
  if (!dragStart) return;
  seedBox = boxFrom(dragStart, evtNorm(e));
  renderDrawLayer();
});
window.addEventListener("mouseup", (e) => {
  if (!dragStart) return;
  seedBox = boxFrom(dragStart, evtNorm(e));
  dragStart = null;
  pendingTrack = null;
  $("seed-readout").textContent = `seed [${seedBox.map(mono).join(", ")}] @ t=${seekT().toFixed(3)}`;
  $("add-ann").disabled = false;
  renderDrawLayer();
});
function boxFrom(a, b) {
  const x = Math.min(a[0], b[0]), y = Math.min(a[1], b[1]);
  const w = Math.abs(b[0] - a[0]), h = Math.abs(b[1] - a[1]);
  const cl = (v) => Math.min(Math.max(v, 0), 1);
  return [cl(x), cl(y), Math.min(w, 1 - cl(x)), Math.min(h, 1 - cl(y))];
}

function renderDrawLayer() {
  draw.innerHTML = "";
  if (seedBox) {
    const [x, y, w, h] = seedBox.map((n) => n * 100);
    svg("rect", { x, y, width: w, height: h, class: "seed-box" }, draw);
  }
  if (pendingTrack && pendingTrack.track && vid.duration) {
    const b = boxAt(pendingTrack.track, seekT());
    if (b) {
      const [x, y, w, h] = b.map((n) => n * 100);
      svg("rect", { x, y, width: w, height: h, class: "track-box" }, draw);
    }
  }
  if (affectPoint) {
    const [x, y] = affectPoint.map((n) => n * 100);
    svg("circle", { cx: x, cy: y, r: 1.2, class: "affect-dot" }, draw);
  }
}
// Interpolate a track box at loop-normalized t (mirrors the preview renderer).
function boxAt(track, t) {
  if (!track || !track.length) return null;
  if (t <= track[0].t) return track[0].box;
  if (t >= track[track.length - 1].t) return track[track.length - 1].box;
  for (let i = 1; i < track.length; i++) {
    if (t <= track[i].t) {
      const a = track[i - 1], b = track[i], f = (t - a.t) / (b.t - a.t);
      return a.box.map((v, j) => v + (b.box[j] - v) * f);
    }
  }
  return track[track.length - 1].box;
}
// Animate the pending track preview while playing.
function previewLoop() { renderDrawLayer(); requestAnimationFrame(previewLoop); }

// --- run the tracker on the seed box ---
$("run-track").addEventListener("click", async () => {
  if (!seedBox || !currentClip) { $("track-readout").textContent = "draw a seed box first"; return; }
  const key = $("ann-key").value.trim() || "detected.object";
  $("track-readout").textContent = "tracking…";
  const resp = await fetch("/api/author/track", {
    method: "POST", headers: { "content-type": "application/json" },
    body: JSON.stringify({
      clip_id: currentClip.id, key, seed_box: seedBox, seed_t: seekT(),
      salience: +$("ann-salience").value, n_keyframes: +$("ann-keyframes").value,
    }),
  });
  if (!resp.ok) { $("track-readout").textContent = "tracker error " + resp.status; return; }
  const ann = await resp.json();
  pendingTrack = { track: ann.track, appear: ann.appear, disappear: ann.disappear };
  $("track-readout").textContent =
    `track: ${ann.track.length} keyframes · window ${mono(ann.appear)}–${mono(ann.disappear)}`;
  $("add-ann").disabled = false;
});

// Add the current seed as a STATIC (untracked) box at the scrub position.
$("add-static").addEventListener("click", () => {
  if (!seedBox) return;
  pendingTrack = { static: true, box: seedBox.slice() };
  $("track-readout").textContent = "static box (no track) at the drawn position";
  $("add-ann").disabled = false;
});

function tiers(ids) {
  const vals = ids.map((id) => $(id).value.trim());
  while (vals.length && !vals[vals.length - 1]) vals.pop();   // drop empty trailing tiers
  return vals.length ? vals : null;
}

$("add-ann").addEventListener("click", () => {
  const key = $("ann-key").value.trim();
  if (!key) { $("track-readout").textContent = "a label key is required"; return; }
  const t = tiers(["t1", "t2", "t3", "t4"]);
  const ann = { key, salience: +$("ann-salience").value, _tiers: t || key };
  if (pendingTrack && pendingTrack.track) {
    ann.track = pendingTrack.track;
    ann.appear = pendingTrack.appear; ann.disappear = pendingTrack.disappear;
  } else if (pendingTrack && pendingTrack.static) {
    ann.box = pendingTrack.box;
  } else if (seedBox) {
    ann.box = seedBox.slice();
  } else { $("track-readout").textContent = "draw + (optionally) track a box first"; return; }
  annotations.push(ann);
  seedBox = null; pendingTrack = null; $("add-ann").disabled = true;
  renderLists(); renderDrawLayer();
});

$("add-aff").addEventListener("click", () => {
  const key = $("aff-key").value.trim();
  if (!key || !affectPoint) return;
  const t = tiers(["a1", "a2", "a3", "a4"]);
  affect.push({ key, at: affectPoint.slice(), min_level: +$("aff-min").value, _tiers: t || key });
  affectPoint = null; $("add-aff").disabled = true;
  renderLists(); renderDrawLayer();
});

function renderLists() {
  const al = $("ann-list"); al.innerHTML = "";
  annotations.forEach((a, i) => {
    const li = document.createElement("li");
    const kind = a.track ? `track ${a.track.length}kf ${mono(a.appear)}–${mono(a.disappear)}` : "static";
    const tip = Array.isArray(a._tiers) ? a._tiers.join(" → ") : a._tiers;
    li.textContent = `${a.key} (s${a.salience}, ${kind}) — ${tip}`;
    li.appendChild(rm(() => { annotations.splice(i, 1); renderLists(); }));
    al.appendChild(li);
  });
  const fl = $("aff-list"); fl.innerHTML = "";
  affect.forEach((f, i) => {
    const li = document.createElement("li");
    const tip = Array.isArray(f._tiers) ? f._tiers.join(" → ") : f._tiers;
    li.textContent = `${f.key} (min ${f.min_level}) — ${tip}`;
    li.appendChild(rm(() => { affect.splice(i, 1); renderLists(); }));
    fl.appendChild(li);
  });
}
function rm(fn) {
  const b = document.createElement("button");
  b.textContent = "✕"; b.className = "rm"; b.onclick = fn;
  return b;
}

// Load whatever is already authored for this clip into the editor.
$("load-existing").addEventListener("click", () => {
  if (!currentClip) return;
  const strings = (currentClip.strings && currentClip.strings.en) || {};
  annotations = (currentClip.annotations || []).map((a) => ({ ...a, _tiers: strings[a.key] || a.key }));
  affect = (currentClip.affect || []).map((f) => ({ ...f, _tiers: strings[f.key] || f.key }));
  renderLists();
});

// Assemble strings.en from each entry's _tiers and POST to the manifest.
$("save").addEventListener("click", async () => {
  if (!currentClip) return;
  const en = {};
  const strip = (x) => { const { _tiers, ...rest } = x; return rest; };
  const anns = annotations.map((a) => { en[a.key] = a._tiers; return strip(a); });
  const affs = affect.map((f) => { en[f.key] = f._tiers; return strip(f); });
  const resp = await fetch("/api/author/clip", {
    method: "POST", headers: { "content-type": "application/json" },
    body: JSON.stringify({ clip_id: currentClip.id, annotations: anns, affect: affs, strings: { en } }),
  });
  $("save-readout").textContent = resp.ok
    ? `saved ${anns.length} labels + ${affs.length} emotions to the manifest ✓`
    : "save error " + resp.status;
  if (resp.ok) {                          // refresh the in-memory clip copy
    clips = (await (await fetch("/api/clips")).json()).clips || [];
    currentClip = clips.find((c) => c.id === currentClip.id) || currentClip;
  }
});

$("clip").addEventListener("change", (e) => selectClip(e.target.value));

async function main() {
  await loadClips();
  requestAnimationFrame(previewLoop);
}
main();
