// The song bank: a set of tracks that between them exercise every input the
// look generator reads.
//
// Why this has to exist. Everything downstream of the audio is a function of
// five summary statistics and a section list, and until now every measurement
// in this project was taken on ONE synthetic track — which turned out to have
// two sections, both quiet, making half the scene library unreachable and the
// resulting numbers wrong. A single song cannot tell you whether the generator
// is varied; it can only tell you what it does with that song.
//
// The bank is built against what the generator actually consumes, not against
// what sounds like a reasonable spread of music:
//
//   summary.bpm           motion bias, animation rate, personality pace
//   summary.meanCentroid  director odds, line weight, softness, bloom, vignette
//   summary.meanFlatness  director odds, chroma, saturation, surface texture
//   summary.dynamicRange  feedback amount and decay, contrast
//   summary.meanLoudness  per-section energy, temperament intensity
//   section kinds         which families each director opens — the casting gate
//
// Coverage is a claim, so `tools/song-coverage.js` measures it rather than
// trusting this table. Each entry names the corner it is here to occupy; if a
// song stops occupying it, that is a bug in the synth, and the coverage tool is
// what catches it.
//
// These are test signals, not music. They exist to be measured.

import { FeatureTrack } from './FeatureTrack.js';
import { synthesizeSong } from './synth.js';

/**
 * @typedef {object} SongSpec
 * @property {string} name
 * @property {string} covers  the corner of the feature space this one holds
 */
export const SONGS = [
    // --- slow, quiet, spacious -------------------------------------------
    { name: 'drone',   bpm: 62,  arrangement: 'ambient', brightness: 0.05, noise: 0.02, dynamics: 0.95, transition: 0.05,
      covers: 'beatless · darkest · most dynamic' },
    { name: 'air',     bpm: 76,  arrangement: 'ambient', brightness: 0.90, noise: 0.25, noiseColour: 0.15, dynamics: 0.85, transition: 0.1,
      covers: 'beatless but bright — brightness without a beat' },
    { name: 'ember',   bpm: 68,  arrangement: 'club',    brightness: 0.20, noise: 0.05, dynamics: 0.55, transition: 0.8,
      covers: 'slowest track with a detectable beat — anchors the low tempo end' },
    { name: 'elegy',   bpm: 84,  arrangement: 'ballad',  brightness: 0.10, noise: 0.03, dynamics: 0.90, transition: 0.12,
      covers: 'slow, dark, one build and one payoff' },
    { name: 'hymn',    bpm: 92,  arrangement: 'ballad',  brightness: 0.80, noise: 0.20, dynamics: 0.70, transition: 0.25,
      covers: 'slow and bright — separates tempo from brightness' },
    { name: 'still',   bpm: 100, arrangement: 'sparse',  brightness: 0.35, noise: 0.06, dynamics: 0.75, transition: 0.2,
      covers: 'the degenerate two-section case, which must not break' },

    // --- mid tempo, the bulk of the space ---------------------------------
    { name: 'dusk',    bpm: 118, arrangement: 'classic', brightness: 0.02, noise: 0.01, dynamics: 0.60, transition: 0.45,
      covers: 'darkest and most tonal with a full arrangement' },
    { name: 'centre',  bpm: 124, arrangement: 'classic', brightness: 0.50, noise: 0.20, dynamics: 0.55, transition: 0.6,
      covers: 'the middle of every axis — the null hypothesis' },
    { name: 'glare',   bpm: 128, arrangement: 'classic', brightness: 0.95, noise: 0.55, dynamics: 0.50, transition: 0.9,
      covers: 'bright and noisy together' },
    { name: 'slab',    bpm: 126, arrangement: 'club',    brightness: 0.45, noise: 0.25, dynamics: 0.05, transition: 0.95,
      covers: 'limitered — lowest dynamic range, longest sections' },
    { name: 'chrome',  bpm: 132, arrangement: 'club',    brightness: 0.85, noise: 0.06, noiseColour: 0.1, dynamics: 0.35, transition: 0.85,
      covers: 'bright and tonal at club tempo' },
    { name: 'murk',    bpm: 88,  arrangement: 'classic', brightness: 0.12, noise: 0.85, noiseColour: 0.95, dynamics: 0.70, transition: 0.35,
      covers: 'dark tonal content under bright hiss — the quadrant that breaks the brightness/noise correlation' },

    // --- fast, dense, broken ----------------------------------------------
    { name: 'lattice', bpm: 140, arrangement: 'breaks',  brightness: 0.60, noise: 0.35, dynamics: 0.45, transition: 0.92,
      covers: 'most sections — makes rosters actually rotate' },
    { name: 'grit',    bpm: 150, arrangement: 'breaks',  brightness: 0.15, noise: 0.90, noiseColour: 0.85, dynamics: 0.40, transition: 0.88,
      covers: "noisiest and dark — the corrupt director's home ground" },
    { name: 'plate',   bpm: 138, arrangement: 'club',    brightness: 0.25, noise: 0.70, noiseColour: 0.9, dynamics: 0.10, transition: 0.95,
      covers: 'noisy AND compressed — two extremes at once' },
    { name: 'runner',  bpm: 174, arrangement: 'breaks',  brightness: 0.70, noise: 0.40, dynamics: 0.50, transition: 0.9,
      covers: 'fastest with a broken arrangement' },
    { name: 'flare',   bpm: 168, arrangement: 'classic', brightness: 0.95, noise: 0.85, dynamics: 0.25, transition: 0.7,
      covers: 'brightest and noisiest — the far corner' },
];

const DURATION = 120;

// Analysis is CPU-bound seconds per track and every caller wants the same
// tracks, so the bank is built once and held.
const cache = new Map();

/** One song from the bank, analysed. */
export function song(name, { fps = 60, duration = DURATION } = {}) {
    const cacheKey = `${name}:${fps}:${duration}`;
    if (cache.has(cacheKey)) return cache.get(cacheKey);
    const spec = SONGS.find((s) => s.name === name);
    if (!spec) throw new Error(`no song named "${name}" — have: ${SONGS.map((s) => s.name).join(', ')}`);

    const entry = {
        ...spec,
        track: FeatureTrack.fromAudioBuffer(synthesizeSong({
            bpm: spec.bpm,
            duration,
            arrangement: spec.arrangement,
            brightness: spec.brightness,
            noise: spec.noise,
            noiseColour: spec.noiseColour ?? null,
            dynamics: spec.dynamics,
            transition: spec.transition ?? 0.5,
            seed: 1 + SONGS.indexOf(spec),
        }), { fps }),
    };
    cache.set(cacheKey, entry);
    return entry;
}

/**
 * The whole bank, analysed.
 *
 * `count` takes an evenly spaced subset rather than the first n, so a cheap run
 * still spans the space instead of testing five slow ambient tracks.
 */
export function songBank({ count = null, fps = 60, duration = DURATION } = {}) {
    const specs = count && count < SONGS.length
        ? Array.from({ length: count }, (_, i) => SONGS[Math.round(i * (SONGS.length - 1) / (count - 1))])
        : SONGS;
    return specs.map((s) => song(s.name, { fps, duration }));
}

/** Feature axes the bank claims to span, and where each is read. */
export const AXES = [
    { key: 'bpm', label: 'tempo', of: (t) => t.summary.bpm, range: [60, 180],
      reads: 'motion bias · animation rate · personality pace' },
    // Not [0,1]: centroid is mapped to a LOG frequency axis, so a track made of
    // nothing but sub-bass and a 55Hz pad still measures ~0.28 and pure hiss
    // measures ~0.9. The range here is what the statistic can really take on for
    // music, and calling it [0,1] would report a permanent 50% gap that no
    // synth change could close.
    { key: 'centroid', label: 'brightness', of: (t) => t.summary.meanCentroid, range: [0.25, 0.95],
      reads: 'director odds · line weight · softness · bloom · vignette' },
    { key: 'flatness', label: 'noisiness', of: (t) => t.summary.meanFlatness, range: [0, 1],
      reads: 'director odds · chroma · saturation · surface texture' },
    { key: 'dynamics', label: 'dynamic range', of: (t) => t.summary.dynamicRange, range: [0, 1],
      reads: 'feedback amount and decay · contrast' },
    // Raw mean spectral magnitude, not a normalised 0..1 — biasFor divides
    // section energy by it, so its scale is whatever the analyser produces.
    { key: 'loudness', label: 'loudness', of: (t) => t.summary.meanLoudness, range: [0, 0.012],
      reads: 'section energy · temperament intensity' },
    { key: 'sections', label: 'section count', of: (t) => t.sections.length, range: [2, 8],
      reads: 'roster rotation · how often the image is allowed to change' },
];
