
/** Re-read the payload for the loaded track without emptying the panel —
 *  a switch flips and the words stay on screen. The cache only, never the
 *  network. */
async function reread() {
    const id = identity();
    if (!id) return;
    const g = gen;
    try {
        const p = await api.lyricsGet({ title: id.title, artist: id.artist, album: id.album, durationSecs: id.durationSecs, path: id.path, refresh: null });
        if (g === gen) apply(p, true);
    } catch { /* what is showing stands */ }
}

/** The app's automatic timing correction, on or off for this track. The
 *  switch flips at once; the backend's own number comes with the re-read. */
export async function setAutoSync(enabled) {
    const id = identity();
    if (!id) return;
    const before = lyr.autoSyncOff;
    lyr.autoSyncOff = !enabled;
    try {
        await api.lyricsSetAutoSync(id.title, id.artist, id.durationSecs, enabled);
    } catch (e) {
        lyr.autoSyncOff = before;
        player.error = String(e);
        return;
    }
    await reread();
}

/* ── the vocal-reduced copy ──────────────────────────────────────────────
   The desktop's offline DSP: a band-limited centre cancel, not a learned
   separator, and the wording stays honest about that. The copy plays under
   the SAME title, artist and duration, so the lyric identity is still the
   recording's and the words keep following it. The track starts again from
   the top — there is no seeking a file the engine has only just been given. */
async function swapPath(e, to) {
    /* a COPY of the entry's track, never the row itself: tracksOf caches one
       array per record and hands the SAME objects to the record page and to
       the queue, so writing the path in place would leave the record pointing
       at a cache file the LRU drops out from under it. The desktop does not
       rewrite its queue item either — it plays the copy as its own thing. */
    e.t = { ...e.t, path: to };
    await playQueueAt(player.qi);
}
export async function reduceVocals() {
    const e = current();
    const t = e?.t;
    if (!t?.path || t.stream || lyr.reducing) return false;
    const from = t.path;
    lyr.reducing = true;
    note("Reducing the vocals — this can take a moment");
    try {
        const res = await api.lyricsPrepareInstrumental(from);
        /* the render takes seconds: starting the OLD track's copy now would
           hijack whatever the queue has moved on to */
        const now = current();
        if (!res?.path || now?.t?.path !== from) {
            note("The track changed while preparing — try again");
            return false;
        }
        lyr.reduced = { from, to: res.path };
        await swapPath(now, res.path);
        return true;
    } catch (err) {
        player.error = String(err);
        return false;
    } finally {
        lyr.reducing = false;
    }
}
export async function backToOriginal() {
    const r = lyr.reduced;
    const e = current();
    if (!r || e?.t?.path !== r.to) return false;
    await swapPath(e, r.from);
    lyr.reduced = null;
    return true;
}

/* ── the tap-to-sync room ────────────────────────────────────────────────
   The song plays and one tap lands per line; the taps ARE the timing.
   Saving writes the track's own document (lyrics_save_lines), the top of
   the waterfall, the same shelf a paste lands on — and the backend drops
   every line without a time, so a half-tapped set is stored as the lines
   that have one. */
export const tapSync = $state({
    rows: [],        // [{ time_ms: number|null, text }]
    cursor: 0,       // the row the next tap stamps
    identity: null,  // the track the rows belong to, captured at open
    saving: false,
    error: "",
});

const plainLines = (text) => String(text ?? "").split("\n").map((l) => l.trim())
    .filter((l) => l && !/^\[.*\]$/.test(l));

/** Open the room for the playing track. An already-timed set is re-tapped
 *  from scratch: the room shows three lines, not a list, so it has no
 *  per-line handle to nudge — it has the tap. */
export function openTapSync() {
    const id = identity();
    if (!id) return;
    /* peeled by the back gesture mid-tap: the same track's stamps come back
       rather than the room starting over */
    if (tapSync.identity?.key === id.key && tapSync.rows.some((r) => r.time_ms != null)) { openTiming(); return; }
    const texts = (lyr.lines.length ? lyr.lines.map((l) => l.text) : plainLines(lyr.plain))
        .filter((t) => String(t || "").trim());
    if (!texts.length) return;
    tapSync.rows = texts.map((text) => ({ time_ms: null, text }));
    tapSync.cursor = 0;
    tapSync.identity = id;
    tapSync.saving = false;
    tapSync.error = "";
    openTiming();
}
export function closeTapSync() {
    closeTiming();
    tapSync.rows = [];
    tapSync.cursor = 0;
    tapSync.identity = null;
    tapSync.saving = false;
    tapSync.error = "";
}
/** What the lyrics sheet says under the row when a set was left mid-tap. */
export function tapProgress() {
    const id = identity();
    if (!id || tapSync.identity?.key !== id.key) return null;
    const done = tapSync.rows.filter((r) => r.time_ms != null).length;
    return done ? { done, total: tapSync.rows.length } : null;
}
/** The taps, as this track's own timed document. */
export async function saveTapSync() {
    const id = tapSync.identity;
    const rows = tapSync.rows.filter((r) => r.time_ms != null && r.text.trim());
    if (!id || !rows.length || tapSync.saving) return false;
    tapSync.saving = true;
    tapSync.error = "";
    try {
        const p = await api.lyricsSaveLines(id.title, id.artist, id.durationSecs,
            rows.map((r) => ({ time_ms: Math.max(0, Math.round(r.time_ms)), text: r.text })));
        /* the same track is still playing: show the result at once */
        if (identity()?.key === id.key && p && p.status) apply(p);
        closeTapSync();
        return true;
    } catch (e) {
        tapSync.error = String(e).replace(/^Error:\s*/, "").slice(0, 140);
        return false;
    } finally {
        tapSync.saving = false;
    }
}
