// The filmstrip: every song in the bank, sampled across its own length.
//
// The gallery answers "does this scene look the same in every song". This
// answers the question one level up and one axis over: does a SONG look the
// same as itself, half an hour of playback later.
//
// Nothing else shows that. The variety harness reduces a video to a number, the
// app shows one video at the speed of the song, and the phase gates never look
// at two moments of the same track side by side. So the failure this exists to
// catch — a video that is doing plenty and going nowhere — has until now been
// something you could only notice by watching four minutes and remembering what
// the first minute looked like.
//
// One frame every thirty seconds, straight across. A strip whose frames could
// be shuffled without anyone noticing is a video with no arc, whatever its
// drift score says. See look/Story.js for the layer meant to fix that, and
// checks/variety/signature.js directionOf for the same question as a number.
//
// Everything renders through the whole normal pipeline — Show, ArcDriver, post,
// the lot — rather than through a bare Engine as the gallery does, because the
// arc IS the subject here.

import { Show, FADE_SECONDS } from '../Show.js';
import { generateLook } from '../look/LookGenerator.js';
import { song, SONGS } from '../audio/songbank.js';
import { hashString } from '../engine/rng.js';
import { frameDescriptor, motionDescriptor } from './variety/descriptors.js';
import { descriptorDistance, directionOf, STRUCTURAL } from './variety/signature.js';
import { describeStory } from '../look/Story.js';

export const STRIP = { width: 256, height: 144 };

/** Seconds between frames. Thirty is a section or two at most tempos. */
export const DEFAULT_EVERY = 30;

/**
 * How long a song is synthesised for.
 *
 * Long enough to have an arc to sample, short enough that the whole bank
 * finishes in one sitting: every extra thirty seconds is another frame for each
 * of seventeen songs, and each frame costs its warm-up. `?duration=240` for the
 * fuller version when a specific song is in question.
 */
export const DEFAULT_DURATION = 180;

/**
 * Frames of warm-up before each probe.
 *
 * Feedback and any stateful layer need to be converged or the probe measures
 * the trail of a black frame — a structure every probe shares, which would make
 * the whole strip look more alike than it is. Half of what the variety harness
 * uses, because this is 17 songs deep and the cost is linear.
 */
const WARMUP = 24;

export const songNames = () => SONGS.map((s) => s.name);

const mean = (xs) => (xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0);

function blockMean(byBlock) {
    const values = STRUCTURAL.map((b) => byBlock[b]).filter((v) => v !== undefined);
    return mean(values);
}

/**
 * Render one song's strip.
 *
 * @returns {{name, frames: Array, drift: number, direction: number, look}}
 */
export function renderSong(show, name, { duration, every }) {
    const { track } = song(name, { duration });
    // Seeded off the song's name, so a strip is stable between rebuilds and two
    // songs are not accidentally handed the same look.
    const look = generateLook(track, { seed: hashString(name) });
    show.useTrack(track, look);

    const frames = [];
    const descriptors = [];
    // Pulled clear of the opening and closing fades. A probe at t=0 reads a
    // black frame — correct output, useless sample — and the same at the very
    // end, so the first and last cells sit just inside the video proper. The
    // caption shows the time actually sampled rather than the nominal one.
    const inside = (t) => Math.min(
        Math.max(t, FADE_SECONDS + 0.5),
        Math.max(0, track.duration - FADE_SECONDS - 0.5));

    for (let nominal = 0; nominal < track.duration - 0.5; nominal += every) {
        const t = inside(nominal);
        const frame = Math.min(track.frameCount - 2, Math.round(t * track.fps));
        show.warmUp(frame, WARMUP);

        const pixels = Uint8Array.from(show.readPixels(show.renderFrame(frame)));
        // A second frame five later, so the motion block is measured rather
        // than silently scored zero — the same omission the gallery had.
        const moved = Uint8Array.from(show.readPixels(show.renderFrame(frame + 5)));

        const state = show.arc.state;
        const sectionIndex = track.sectionIndexAt(frame);
        const section = look.sections[sectionIndex] || {};
        frames.push({
            time: t,
            frame,
            pixels,
            kind: state.kind || section.kind || '',
            scene: state.sceneName || '',
            act: state.act || '',
            tension: state.tension ?? 0.5,
            journey: state.journey ?? 0,
            moment: momentAt(look.story, sectionIndex),
        });

        const still = frameDescriptor(pixels, STRIP.width, STRIP.height);
        const motion = motionDescriptor(pixels, moved, STRIP.width, STRIP.height);
        descriptors.push({ ...still, motion: motion.scale.concat(motion.layout) });
    }

    // The two numbers that say what the strip shows, on the same descriptor the
    // variety harness uses: how far this video gets from itself, and whether
    // that distance grows with time or is just churn.
    const gaps = [];
    const dists = [];
    for (let i = 0; i < descriptors.length; i++) {
        for (let j = i + 1; j < descriptors.length; j++) {
            gaps.push(frames[j].time - frames[i].time);
            dists.push(blockMean(descriptorDistance(descriptors[i], descriptors[j])));
        }
    }

    return {
        name,
        frames,
        drift: mean(dists),
        direction: directionOf(gaps, dists),
        // How much the FIRST half of the video resembles the last — the reading
        // that separates a video which ends somewhere new from one that has
        // come back on purpose. A recap should show up here and nowhere else.
        endsApart: dists.length ? dists[dists.length - 1] : 0,
        story: look.story,
        storyLine: describeStory(look.story),
        director: look.director,
        bpm: Math.round(track.summary.bpm),
        sections: look.sections.length,
    };
}

function momentAt(story, index) {
    if (!story || !story.moments) return '';
    const m = story.moments;
    if (index === m.climax) return 'climax';
    if (index === m.turn) return 'turn';
    if (index === m.arrival) return 'arrival';
    if (index === m.resolution) return 'resolution';
    return '';
}

/**
 * Build every strip, reporting each song as it lands.
 *
 * One Show for the whole run: `useTrack` replaces the track and rebuilds the
 * arc, and constructing a Show per song would rebuild the GL context seventeen
 * times for nothing.
 */
export async function buildFilmstrip({
    duration = DEFAULT_DURATION, every = DEFAULT_EVERY, names = null, onSong = null,
} = {}) {
    const list = names && names.length ? names : songNames();
    const show = new Show({ ...STRIP });
    const rows = [];
    try {
        for (let i = 0; i < list.length; i++) {
            let row;
            try {
                row = renderSong(show, list[i], { duration, every });
            } catch (err) {
                row = { name: list[i], frames: [], drift: 0, direction: 0, error: err.message };
            }
            rows.push(row);
            if (onSong) onSong(i + 1, list.length, row);
            // Yield, so a row paints as it lands instead of the page freezing
            // for the whole run and then showing everything at once.
            await new Promise((r) => setTimeout(r, 0));
        }
    } finally {
        show.dispose();
    }
    return rows;
}
