// Colour movement across a track.
//
// The palette was generated once and pushed to every layer of every section for
// the entire runtime. Five minutes, one colour scheme, no movement — and colour
// is the strongest perceptual variable the system has, so freezing it wastes
// the biggest lever available for making a long video feel like it is going
// somewhere.
//
// The fix is deliberately NOT "a new palette per section". A track has one
// identity and the palette is most of it; replacing it mid-video reads as a
// different video. What moves is the palette itself — rotated, warmed, opened
// up — so at four minutes the image is somewhere the first minute implied.
//
// Everything here is bounded on purpose. A full hue rotation would destroy the
// identity as surely as a new palette; the movement has to be the kind you
// notice on a rewatch rather than the kind you notice as an effect.

/**
 * Ceilings on the whole mechanism. Nothing downstream may exceed these, and the
 * Phase 11 gate samples the movement against the palette contrast floor rather
 * than trusting them.
 */
export const MAX_HUE_ROTATION = 0.6;     // radians, ~34 degrees
export const MAX_CHROMA_SCALE = 0.35;    // ±35% saturation
export const MAX_LIGHT_SHIFT = 0.07;     // OKLCH lightness

export const ARC_MODES = ['static', 'drift', 'sections', 'lift'];

/**
 * How this track's colour moves.
 *
 * 'static' stays rare. This layer exists because nothing moved, and a library
 * where most tracks still do not move would not have fixed anything — but a
 * track whose colour holds is a legitimate choice and one in six or so gets it.
 */
export function derivePaletteArc(summary, rng) {
    const mode = rng.pickWeighted(ARC_MODES, [1.5, 3, 3, 2.5]);
    const dir = rng.bool() ? 1 : -1;

    return {
        mode,
        // Total hue travel from the first frame to the last, for 'drift'.
        hueTravel: dir * rng.range(0.25, MAX_HUE_ROTATION),
        // Per-section-kind offsets, for 'sections': drops consistently warmer
        // or cooler than breakdowns, so the colour tells you where you are.
        kindHue: {
            intro:     rng.range(-0.2, 0.2),
            build:     rng.range(-0.3, 0.3),
            drop:      dir * rng.range(0.2, MAX_HUE_ROTATION),
            sustain:   rng.range(-0.25, 0.25),
            breakdown: -dir * rng.range(0.1, 0.4),
            outro:     rng.range(-0.3, 0.3),
        },
        // For 'lift': how much energy opens the colour up. Saturation rising
        // into a drop is the single most legible colour gesture available.
        chromaLift: rng.range(0.12, MAX_CHROMA_SCALE),
        lightLift: rng.range(0.02, MAX_LIGHT_SHIFT),
        // Every mode carries a little of the slow drift underneath, so even a
        // 'sections' track is not the same colour at the end as at the start.
        underDrift: dir * rng.range(0.05, 0.2),
    };
}

/**
 * The colour shift for one frame, as arguments to palette.shiftPalette.
 *
 * Deterministic in progress and features only — no state, no random source —
 * the same rule the rest of the render path follows.
 *
 * @param {object} arc          from derivePaletteArc
 * @param {object} ctx
 * @param {number} ctx.progress     0..1 through the track
 * @param {string} ctx.sectionKind  kind of the section this frame is in
 * @param {object} ctx.features     FeatureTrack row
 */
export function paletteShiftAt(arc, { progress = 0, sectionKind = '', features = null } = {}) {
    if (!arc) return { hue: 0, chroma: 1, lightness: 0 };

    // The slow underlying travel, present in every mode. Eased rather than
    // linear so the move is least visible at the ends, where a cut to the
    // opening image would otherwise expose it.
    const eased = progress * progress * (3 - 2 * progress);
    let hue = arc.underDrift * eased;
    let chroma = 1;
    let lightness = 0;

    if (arc.mode === 'drift') {
        hue = arc.hueTravel * eased;
    } else if (arc.mode === 'sections') {
        // Held per section rather than ramped: the colour changing AT the cut
        // is the point, and a ramp would smear it into nothing.
        hue += arc.kindHue[sectionKind] ?? 0;
    } else if (arc.mode === 'lift') {
        const energy = features ? (features.sectionEnergy ?? 0) : 0;
        const build = features ? (features.buildSlope ?? 0) : 0;
        const drive = Math.min(1, energy * 0.7 + build * 0.6);
        chroma = 1 + arc.chromaLift * (drive * 2 - 1);
        lightness = arc.lightLift * (drive - 0.35);
        hue += arc.kindHue[sectionKind] * 0.4 || 0;
    }

    return {
        hue: clamp(hue, -MAX_HUE_ROTATION, MAX_HUE_ROTATION),
        chroma: clamp(chroma, 1 - MAX_CHROMA_SCALE, 1 + MAX_CHROMA_SCALE),
        lightness: clamp(lightness, -MAX_LIGHT_SHIFT, MAX_LIGHT_SHIFT),
    };
}

function clamp(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); }

/** One line for the HUD and check output. */
export function describePaletteArc(arc) {
    if (!arc || arc.mode === 'static') return 'colour: held';
    if (arc.mode === 'drift') return `colour: drift ${(arc.hueTravel * 57.3).toFixed(0)}°`;
    if (arc.mode === 'lift') return `colour: lift ±${(arc.chromaLift * 100).toFixed(0)}% sat`;
    return `colour: per-section ±${(Math.max(...Object.values(arc.kindHue).map(Math.abs)) * 57.3).toFixed(0)}°`;
}
