// How much does the IDENTITY itself vary between songs?
//
// The measured state after Epic 3's first two slices: stages have the lowest
// floor of any arm, so sharing content does make a video look like itself — but
// they do not beat ordinary scenes on the distance BETWEEN songs. The diagnosis
// offered for that was "the identity's expressive range is the bottleneck",
// which is a claim, and claims of that shape have been wrong twice already in
// this epic.
//
// It is also cheap to check, because it needs no GPU. If two songs' identities
// are already far apart and their videos are not, the stages are failing to
// express what they are given and more registers will not help. If the
// identities themselves are clustered, the registers are the problem and the
// diagnosis stands.
//
//   node tools/identity-census.js
//   node tools/identity-census.js 17     use the whole bank

import { songBank } from '../src/audio/songbank.js';
import { generateIdentity, FILLS, LATTICES } from '../src/look/Identity.js';
import { Rng, hashString } from '../src/engine/rng.js';

const COUNT = Number(process.argv[2]) || 12;
const bank = songBank({ count: COUNT });

const identities = bank.map((e) => ({
    name: e.name,
    id: generateIdentity(e.track.summary, new Rng(hashString(e.name)), e.track.sections.length),
}));

/**
 * Identity distance, 0..1, split by register.
 *
 * Categorical decisions (sides, fill, lattice) count as a flat 1 when they
 * differ, because a hexagon is not "nearer" a pentagon than a circle in any way
 * a viewer experiences. Continuous ones are scaled by the range they can take.
 */
function registerDistances(a, b) {
    const cat = (x, y) => (x === y ? 0 : 1);
    const num = (x, y, span) => Math.min(1, Math.abs(x - y) / span);

    const cast = [
        cat(a.cast.protagonist.sides, b.cast.protagonist.sides),
        cat(a.cast.chorus.sides, b.cast.chorus.sides),
        cat(a.cast.protagonist.notchCount > 0, b.cast.protagonist.notchCount > 0),
        cat(a.cast.protagonist.hollow > 0, b.cast.protagonist.hollow > 0),
        num(a.cast.protagonist.round, b.cast.protagonist.round, 0.5),
        num(a.cast.protagonist.elong, b.cast.protagonist.elong, 0.7),
    ];
    const ink = [
        cat(a.ink.fill, b.ink.fill),
        cat(a.ink.outline > 0, b.ink.outline > 0),
        cat(a.ink.posterize > 0, b.ink.posterize > 0),
        num(a.ink.weight, b.ink.weight, 1),
        num(a.ink.edge, b.ink.edge, 1),
    ];
    const staging = [
        cat(a.lattice.kind, b.lattice.kind),
        num(a.lattice.jitter, b.lattice.jitter, 0.8),
        num(a.lattice.spread, b.lattice.spread, 0.7),
        num(a.lattice.scaleSpread, b.lattice.scaleSpread, 1),
        num(Math.log2(a.lattice.elementScale), Math.log2(b.lattice.elementScale), 2.8),
    ];
    const mean = (v) => v.reduce((x, y) => x + y, 0) / v.length;
    return { cast: mean(cast), ink: mean(ink), staging: mean(staging) };
}

const pairs = [];
for (let i = 0; i < identities.length; i++) {
    for (let j = i + 1; j < identities.length; j++) {
        pairs.push({
            a: identities[i].name, b: identities[j].name,
            ...registerDistances(identities[i].id, identities[j].id),
        });
    }
}
const mean = (v) => v.reduce((x, y) => x + y, 0) / v.length;
const bar = (v, w = 24) => '█'.repeat(Math.round(v * w)) + '·'.repeat(w - Math.round(v * w));

console.log(`\n${identities.length} songs · ${pairs.length} pairs\n`);
console.log('IDENTITY DISTANCE BETWEEN SONGS — per register, 0 = the same decisions\n');
for (const reg of ['cast', 'ink', 'staging']) {
    const v = pairs.map((p) => p[reg]);
    const identical = v.filter((x) => x < 0.05).length;
    console.log(`  ${reg.padEnd(8)} ${bar(mean(v))}  ${mean(v).toFixed(2)}` +
        `   min ${Math.min(...v).toFixed(2)} max ${Math.max(...v).toFixed(2)}` +
        `   ${identical} pairs near-identical`);
}
console.log(`  ${'overall'.padEnd(8)} ${bar(mean(pairs.map((p) => (p.cast + p.ink + p.staging) / 3)))}` +
    `  ${mean(pairs.map((p) => (p.cast + p.ink + p.staging) / 3)).toFixed(2)}`);

// --- which decisions are actually being used ------------------------------
// A register can look varied on average while one option takes nine songs in
// ten. Coverage of the option space is the other half of the question.
console.log('\nOPTION COVERAGE — how much of each decision space the bank reaches\n');
const tally = (label, values, space) => {
    const counts = new Map();
    for (const v of values) counts.set(v, (counts.get(v) || 0) + 1);
    const used = [...counts.entries()].sort((x, y) => y[1] - x[1]);
    console.log(`  ${label.padEnd(16)} ${counts.size}/${space.length} used   ` +
        used.map(([k, n]) => `${k}×${n}`).join(' '));
};
tally('ink fill', identities.map((e) => e.id.ink.fill), FILLS);
tally('lattice', identities.map((e) => e.id.lattice.kind), LATTICES);
tally('protagonist', identities.map((e) => String(e.id.cast.protagonist.sides)), ['0', '3', '4', '5', '6', '8']);
tally('element scale', identities.map((e) => (e.id.lattice.elementScale < 0.2 ? 'tiny'
    : e.id.lattice.elementScale > 0.6 ? 'huge' : 'mid')), ['tiny', 'mid', 'huge']);

console.log('\nCLOSEST IDENTITY PAIRS\n');
for (const p of pairs.slice().sort((x, y) => (x.cast + x.ink + x.staging) - (y.cast + y.ink + y.staging)).slice(0, 5)) {
    console.log(`  ${((p.cast + p.ink + p.staging) / 3).toFixed(2)}  ${p.a.padEnd(9)} ≈ ${p.b.padEnd(9)}` +
        `   cast ${p.cast.toFixed(2)} ink ${p.ink.toFixed(2)} staging ${p.staging.toFixed(2)}`);
}

console.log('\nREAD THIS AGAINST THE VISUAL NUMBERS\n');
console.log('  The stages arm separates songs by about 0.10 in visual distance against a');
console.log('  reference of roughly 0.19. If identity distance here is already high, the');
console.log('  stages are not expressing what they are handed and more registers will not');
console.log('  help. If it is low, the registers are the bottleneck and widening them is');
console.log('  the right next move.\n');
