// What the measurements MEAN — the thresholds and the rules, kept apart from
// the numbers they read.
//
// scenes/metadata.json is generated and holds no opinions: it says a scene
// paints 32% of the frame across six songs and nothing about whether that is
// enough. This file is where that becomes a decision, and it is hand-written
// precisely because a threshold is a judgement about the videos rather than a
// fact about the library.
//
// `surface` used to be declared in each scene file, and it drifted: nine scenes
// said `canvas` while painting under a third of the frame, which is how a
// section ended up standing on a few bright things and black. It is derived
// now. A scene cannot lie about what it paints, because nobody writes it down.

import metadata from './metadata.json';
import { readsHistory } from '../params/schema.js';

/**
 * The line between a picture and elements on one.
 *
 * Half the frame. Under it a scene is not somewhere a shot can happen — it is
 * the shot, or something in it, and what would be behind it is black.
 *
 * The same number does both jobs on purpose: `canvas` and "may be a ground" are
 * one idea, and having a scene that is a canvas but not ground-capable would be
 * a distinction with nothing behind it.
 */
export const GROUND_MIN = 0.5;

/**
 * The bias a GROUND is sampled with, wherever it appears.
 *
 * A bed is not sampled like a shot. It keeps a density floor whatever the
 * section wants — an intro biased sparse would otherwise draw the ground at
 * density zero and be thin again for a new reason — and it sits back through
 * energy and motion instead.
 *
 * Defined here rather than in the generator because the measurement has to use
 * it too. Coverage depends enormously on parameters: Metaballs paints 69% of
 * the frame sampled as a drop's shot and 27% sampled as an intro's bed, and a
 * label taken at the first bias tells you nothing about the second. So the
 * number that decides whether a scene may be a ground is measured AS a ground.
 * One definition, both places, or the two silently disagree.
 */
export const GROUND_BIAS = { energy: 0.5, density: 0.45, motion: 0.3 };

/**
 * The hand a GROUND is drawn with.
 *
 * `extremity` is how far toward the ends of its own ranges a track pushes every
 * parameter, and it is the right dial for a shot: the ends are where a scene is
 * most itself. It is the wrong dial for a bed. Half of what decides how much a
 * scene paints is a parameter with no `bias` key at all — Metaballs' threshold,
 * for one — so extremity is the only thing moving it, and one end of that range
 * is an empty frame. Measured: a ground drawn at the track's own temperament
 * landed on threshold 1.85 of 2.0 and painted 6% of the frame, while the same
 * scene measures 58% drawn moderately.
 *
 * So a bed is drawn near the middle and the extremes are left to the shot,
 * which is also the right instinct visually.
 */
export function groundTemperamentFrom(temperament) {
    if (!temperament) return temperament;
    return { ...temperament, extremity: temperament.extremity * 0.2, intensity: 0 };
}

/**
 * The song's hand, as it applies to a GROUND.
 *
 * The ink treatment — hatch, stipple, halftone, hollow — is how the song draws.
 * `hollow` draws outlines and no fill at all, which is a strong, legible
 * identity for the thing the section is ABOUT and a disaster for the thing it
 * stands on: a scene that paints 61% of the frame averaged over six identities
 * paints 2% in the song that asked for outlines, and the section renders black
 * with a ground under it. Measured, holding grounds to their worst identity
 * instead left four castable beds in the whole library.
 *
 * So the world is drawn solid and the hand is kept for the subject. Everything
 * else about the identity — the cast, the lattice, the line weight, the
 * palette — reaches the ground untouched, so it is still unmistakably this
 * song's world.
 */
export function groundPersonalityFrom(personality) {
    if (!personality || !personality.identity || !personality.identity.ink) return personality;
    const { identity } = personality;
    if (identity.ink.fill !== 'hollow') return personality;
    return {
        ...personality,
        identity: { ...identity, ink: { ...identity.ink, fill: 'flat' } },
    };
}

/** The bias `ground` would be sampled with inside `section`. */
export function groundBiasFrom(bias) {
    return {
        ...bias,
        // FLOORS, not reductions, on both of the axes that fill a frame.
        //
        // The first version of this lowered energy to make the bed sit back,
        // which turned out to be precisely how to empty it: scenes bias the
        // parameters that decide how much they paint against `energy` as often
        // as against `density` — Metaballs sizes its blobs on energy, so an
        // intro's ground drew radius 0.14 out of a 0.1–0.6 range and painted 4%
        // of the frame. A bed sits back through MOTION, which costs it nothing
        // in coverage, and through being behind everything else.
        density: Math.max(GROUND_BIAS.density, bias.density),
        energy: Math.max(GROUND_BIAS.energy, bias.energy),
        motion: Math.max(0, (bias.motion ?? 0.5) - 0.2),
    };
}

/**
 * What a scene is, compositionally.
 *
 *   canvas      fills the frame. Belongs underneath, and two of them stacked is
 *               two pictures fighting rather than one picture with depth.
 *   composable  mostly empty by design. Reads as elements ON something, and
 *               shown alone it is a few bright things on black — which scores
 *               well for variety and is thin to watch.
 */
export const SURFACES = ['canvas', 'composable'];

const ROW = (module) => (metadata.scenes && metadata.scenes[module.name]) || null;

/**
 * Coverage for a scene nobody has measured.
 *
 * Two ways to land here: a `layer3d`, which the measuring pass does not render,
 * and a scene added since the last refresh. Deliberately pessimistic — an
 * unmeasured scene must not be able to talk its way into the ground slot, which
 * `canGround` enforces by requiring a real row rather than by trusting this.
 */
const ASSUMED_COVERAGE = 0.15;

export function coverageOf(module) {
    const row = ROW(module);
    return row ? row.coverage : ASSUMED_COVERAGE;
}

/**
 * How much this scene paints when it is used as a BED — measured at
 * GROUND_BIAS, which is the only figure that answers the question `canGround`
 * asks. See GROUND_BIAS for why the two numbers differ so much.
 */
export function groundCoverageOf(module) {
    const row = ROW(module);
    if (!row) return ASSUMED_COVERAGE;
    if (row.bedCoverageMean !== undefined) return row.bedCoverageMean;
    return row.bedCoverage === undefined ? row.coverage : row.bedCoverage;
}

/**
 * What this scene paints as a bed in the IDENTITY THAT SUITS IT LEAST.
 *
 * A song's identity can gut a scene's fill — `hollow` ink draws outlines and no
 * fill, a dark palette drops it under the threshold — and the spread is wide:
 * Metaballs paints 61% averaged over six identities and 14% in the worst of
 * them, which is a black frame with a ground under it.
 *
 * Held as a SEPARATE, lower bar rather than as the eligibility number. Demanding
 * the worst case clear 50% leaves four castable grounds in the whole library —
 * every video in the world standing on one of four beds is a worse failure than
 * an occasional dim intro. So: fills the frame on average, and never vanishes.
 */
export function groundFloorCoverageOf(module) {
    const row = ROW(module);
    if (!row) return ASSUMED_COVERAGE;
    return row.bedCoverage === undefined ? groundCoverageOf(module) : row.bedCoverage;
}

/** A ground may dim under a hostile identity. It may not disappear. */
export const GROUND_FLOOR_MIN = 0.25;

export function isMeasured(module) {
    return ROW(module) !== null;
}

/** Derived, never declared. See the header. */
export function surfaceOf(module) {
    return coverageOf(module) >= GROUND_MIN ? 'canvas' : 'composable';
}

/**
 * Whether a scene can be the ground under a section.
 *
 * Measured to fill the frame, and self-contained. The second half rules out
 * `prev()`: that returns the whole composited previous frame, INCLUDING the
 * layers above this one, so a datamosh under a shot is not grounding it, it is
 * eating it. Measured when one first became a bed — the render stopped
 * reproducing from a seek, and two WebGL contexts diverged by 91/255 against a
 * tolerance of 4.
 */
export function canGround(module) {
    return isMeasured(module)
        && surfaceOf(module) === 'canvas'
        && groundCoverageOf(module) >= GROUND_MIN
        && groundFloorCoverageOf(module) >= GROUND_FLOOR_MIN
        && !readsHistory(module);
}

/**
 * How unalike two scenes are, structurally — 0 is the same picture.
 *
 * Both profiles are means over the same six songs, so this is a fair comparison
 * in a way that two arbitrary renders would not be. Colour is excluded: six
 * palettes would otherwise make any two scenes look unalike, and the whole
 * point is to find pairs that differ in STRUCTURE.
 *
 * This is the quantity that makes a stack interesting rather than merely full.
 * Two scenes at 0.02 layered over each other are one picture at double density;
 * the same two at 0.3 are a picture with something happening in it.
 */
export function structuralDistance(a, b) {
    const rowA = ROW(a), rowB = ROW(b);
    if (!rowA || !rowB || !rowA.profile || !rowB.profile) return null;

    let total = 0, blocks = 0;
    for (const block of ['scale', 'orient', 'layout', 'region', 'texture']) {
        const x = rowA.profile[block], y = rowB.profile[block];
        if (!x || !y || x.length !== y.length) continue;
        // Chi-square, matching what variety/signature.js compares descriptors
        // with. Duplicated rather than imported because this runs inside the
        // generator and the checks must not be a dependency of it.
        let d = 0;
        for (let i = 0; i < x.length; i++) {
            const sum = x[i] + y[i];
            if (sum > 1e-9) d += ((x[i] - y[i]) ** 2) / sum;
        }
        total += Math.min(1, d / 2);
        blocks++;
    }
    return blocks ? total / blocks : null;
}

/** The variety score from the gallery: how much a scene changes between songs. */
export function varietyOf(module) {
    const row = ROW(module);
    return row ? row.variety : 0;
}

export const METADATA = metadata;
