// The gallery: every visualizer, six times, on six different songs' content.
//
// The complaint this answers is one a metric could not raise — watching a few
// videos, certain scenes announce themselves. You have seen that one before.
// You cannot tell which ones from a number, because the harness measures whole
// videos and a scene that always looks like itself is averaged in with
// everything around it.
//
// So this renders each scene under six complete identities — six casts, six ink
// treatments, six lattices, six palettes, six parameter draws — and puts them
// side by side. A scene whose six thumbnails are interchangeable is a scene the
// song cannot change, and that is the definition of the problem.
//
// Then it scores them, using the same structural descriptor the variety harness
// runs on, so the gallery can be sorted worst-first. Browsing sixty-six scenes
// looking for the repetitive ones is exactly the job a sort order should do.

import { Engine } from '../engine/Engine.js';
import { scenes } from '../scenes/registry.js';
import { sampleValues } from '../params/schema.js';
import { Rng, hashString } from '../engine/rng.js';
import { featureProviderFor } from '../audio/FeatureTrack.js';
import { songBank } from '../audio/songbank.js';
import { generateLook } from '../look/LookGenerator.js';
import { describeIdentity } from '../look/Identity.js';
import { frameDescriptor, motionDescriptor } from './variety/descriptors.js';
import { descriptorDistance, STRUCTURAL } from './variety/signature.js';

const THUMB = { width: 256, height: 144 };

/**
 * The floor a visualizer has to clear: how different its own six frames must be
 * from each other, across six songs' identities.
 *
 * Below this a scene is the same picture wherever it appears, and because it is
 * cast into many songs that repetition leaks between them — the viewer who
 * watches two videos recognises the shot rather than the song. It is a property
 * of the scene, not of the generator, so it is the scene that has to be lifted.
 *
 * 0.04 is a starting bar rather than a derived one. Set where the flat cluster
 * measurably sits, to be raised as scenes are brought up to it.
 */
export const MIN_VARIETY = 0.04;

/**
 * Six contexts, one per song: everything a scene is handed when it is cast.
 *
 * Built from real bank entries rather than invented, so what the gallery shows
 * is what the generator would actually produce — the same identities, palettes
 * and section biases, only with the scene held fixed instead of chosen.
 */
export function galleryContexts(count = 6) {
    return songBank({ count }).map((entry) => {
        const look = generateLook(entry.track, { seed: hashString(entry.name) });
        // The busiest section, because that is where a scene is asked for the
        // most and where two scenes are most likely to converge.
        const section = look.sections.reduce(
            (best, s) => (s.bias.energy > best.bias.energy ? s : best), look.sections[0]);
        return {
            name: entry.name,
            track: entry.track,
            palette: look.palette,
            personality: look.personality,
            bias: section.bias,
            frame: section.startFrame + Math.floor((section.endFrame - section.startFrame) * 0.5),
            identity: describeIdentity(look.personality.identity),
        };
    });
}

/** Bottom-up WebGL pixels into a canvas the right way up. */
function blit(canvas, pixels, width, height) {
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext('2d');
    const image = ctx.createImageData(width, height);
    const row = width * 4;
    for (let y = 0; y < height; y++) {
        const src = (height - 1 - y) * row;
        image.data.set(pixels.subarray(src, src + row), y * row);
    }
    ctx.putImageData(image, 0, 0);
}

/**
 * Render one scene across every context.
 *
 * @returns {{thumbs: Uint8Array[], variety: number, byBlock: object}}
 */
export function renderScene(engine, module, contexts) {
    const thumbs = [];
    const descriptors = [];

    for (const ctx of contexts) {
        engine.timeline.setDuration(ctx.track.duration);
        engine.setFeatureProvider(featureProviderFor(ctx.track));

        const rng = new Rng(hashString(`${module.name}:${ctx.name}`));
        const params = sampleValues(module, rng, ctx.bias, ctx.personality.temperament);

        engine.setLayerSpecs([{
            module,
            params,
            seed: rng.int(0, 0x7fffffff),
            opacity: 1,
            blend: 'normal',
            palette: ctx.palette,
            personality: ctx.personality,
        }]);
        engine.compositor.reset();
        // A few frames of warm-up so anything with state is past its first frame.
        for (let f = ctx.frame - 6; f < ctx.frame; f++) engine.renderFrame(f);
        const pixels = Uint8Array.from(engine.readPixels(engine.renderFrame(ctx.frame)));
        // A second frame, so MOTION is measured rather than silently scored zero.
        //
        // The descriptor has five structural blocks and this only ever built
        // four of them, so `motion` came back 0.000 for every scene in the
        // library and dragged the mean down by a fifth across the board. It
        // looked like a property of the scenes; it was a missing render.
        const moved = Uint8Array.from(engine.readPixels(engine.renderFrame(ctx.frame + 5)));

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

    // How different this scene's six outputs are from each other, on the same
    // structural descriptor the variety harness uses — colour excluded, because
    // six palettes would otherwise make every scene look varied.
    const byBlock = {};
    let total = 0, pairs = 0;
    for (let i = 0; i < descriptors.length; i++) {
        for (let j = i + 1; j < descriptors.length; j++) {
            const d = descriptorDistance(descriptors[i], descriptors[j]);
            for (const b of [...STRUCTURAL, 'colour']) byBlock[b] = (byBlock[b] || 0) + (d[b] || 0);
            total += STRUCTURAL.reduce((a, b) => a + (d[b] || 0), 0) / STRUCTURAL.length;
            pairs++;
        }
    }
    for (const b of Object.keys(byBlock)) byBlock[b] /= pairs || 1;

    return { thumbs, variety: pairs ? total / pairs : 0, byBlock };
}

/**
 * Build the whole gallery, reporting progress as it goes.
 *
 * @param {(done:number, of:number, name:string, row:object) => void} onScene
 */
export async function buildGallery({ contexts, onScene, only = null } = {}) {
    const list = only
        ? scenes.filter((m) => only.includes(m.name))
        : scenes.filter((m) => m.kind === 'fragment');

    const engine = new Engine({ ...THUMB });
    const rows = [];
    try {
        for (let i = 0; i < list.length; i++) {
            const module = list[i];
            let row;
            try {
                row = { module, ...renderScene(engine, module, contexts) };
            } catch (err) {
                row = { module, thumbs: [], variety: 0, byBlock: {}, error: err.message };
            }
            rows.push(row);
            if (onScene) onScene(i + 1, list.length, module.name, row);
            // Yield so the page can paint each row as it lands rather than
            // freezing for a minute and then showing everything at once.
            await new Promise((r) => setTimeout(r, 0));
        }
    } finally {
        engine.dispose();
    }
    return rows;
}

export { blit, THUMB };
