// The song's IDENTITY: the content it is made of, and the hand it is drawn in.
//
// This is the Epic 3 inversion. A scene used to be self-contained — it knew how
// to make metaballs and needed nothing from the track but a palette — and the
// personality could only ever be a set of modifiers layered on an image the
// shader already had. Measured, that is why two songs came out about as
// different from each other as one video is from itself: the generator was
// varying the CONTAINER and never the content.
//
// So the song generates content first, and a stage is a way of arranging
// content it is given.
//
//   CAST   what is on screen. A protagonist and a chorus, as actual forms with
//          sides, notches and hollows, not as hints a shader may consult.
//   INK    how they are drawn. Weight, edge, fill treatment, outline,
//          posterisation — the hand, which changes every pixel of every stage
//          at once and does it structurally rather than chromatically.
//
// Both travel as uniforms, which makes them data rather than code and means any
// stage can consume them without knowing anything about any other stage. The
// rule that keeps this honest, from EPIC-3.md §2:
//
//   An artifact is content a stage could not have invented for itself.
//
// A stage that renders acceptably with the cast replaced by a default is using
// it as a modifier and will drift back into ignoring it, exactly the way most of
// the library ignores u_sigSides today.

/** Fill treatments, as the shader's `u_inkFill` index. */
export const FILLS = ['flat', 'ramp', 'hatch', 'stipple', 'halftone', 'hollow'];

/** Lattices, as the shader's `u_latKind` index. */
export const LATTICES = ['grid', 'radial', 'spiral', 'scatter', 'strata'];

const clamp01 = (x) => Math.max(0, Math.min(1, x));

/**
 * One cast member.
 *
 * `notches` and `hollow` are what take this past the existing signature form.
 * A rounded pentagon is a shape; a pentagon with six notches cut into it and a
 * hole through the middle is a CHARACTER — recognisable across stages, which is
 * the entire point of sharing it.
 */
function castMember(rng, { angular, intricate, solid }) {
    const sides = rng.pickWeighted(
        [0, 3, 4, 5, 6, 8],
        [3 + (1 - angular) * 5, 1 + angular * 2, 2 + angular * 2,
         1 + angular * 2, 1.5 + angular * 2, 0.5 + angular * 1.5]);
    return {
        sides,
        round: clamp01(rng.range(0.05, 0.55) * (1.3 - angular * 0.6)),
        elong: rng.range(0.8, 1.5),
        tilt: rng.range(0, Math.PI),
        // A notched form reads as made rather than as found.
        notchCount: rng.bool(0.25 + intricate * 0.5)
            ? rng.pickWeighted([3, 4, 5, 6, 8, 12], [2, 3, 2, 3, 2, 1]) : 0,
        notchDepth: rng.range(0.06, 0.1 + intricate * 0.22),
        // Hollow forms are the difference between a blob library and a
        // recognisable one, and they cost nothing to draw.
        hollow: rng.bool(0.45 - solid * 0.3) ? rng.range(0.15, 0.6) : 0,
    };
}

/**
 * @param {object} summary  FeatureTrack summary
 * @param {Rng} rng
 * @param {number} sections
 */
export function generateIdentity(summary, rng, sections = 4) {
    const bright = summary.meanCentroid ?? 0.5;
    const noisy = Math.min(1, (summary.meanFlatness ?? 0.2) * 3);
    const fast = clamp01(((summary.bpm ?? 120) - 80) / 80);
    const dynamic = clamp01(summary.dynamicRange ?? 0.5);
    const busy = clamp01((sections - 2) / 5);

    // The audio sets the centre of each decision and the seed picks within it —
    // the same arrangement the personality values use, and for the same reason:
    // deriving outright would buy coupling by destroying seed variety.
    const angular = clamp01(noisy * 0.6 + fast * 0.3 + rng.range(-0.25, 0.25));
    const intricate = clamp01(busy * 0.5 + bright * 0.3 + rng.range(-0.3, 0.3));
    const solid = clamp01(0.5 - dynamic * 0.4 + rng.range(-0.25, 0.25));

    const protagonist = castMember(rng.fork('protagonist'), { angular, intricate, solid });
    // The chorus is a relative of the protagonist, not a stranger: it shares the
    // family and differs in proportion, which is what makes a frame full of them
    // read as one production rather than as two libraries stacked.
    const chorusRng = rng.fork('chorus');
    const chorus = {
        ...castMember(chorusRng, { angular, intricate, solid }),
        sides: chorusRng.bool(0.6) ? protagonist.sides : chorusRng.pick([0, 3, 4, 6]),
        tiltOffset: chorusRng.range(-0.6, 0.6),
    };

    const ink = {
        // Line weight and edge hardness: the two decisions a viewer reads as
        // "what this was drawn with".
        weight: clamp01(0.15 + noisy * 0.35 + rng.range(-0.15, 0.35)),
        edge: clamp01(0.3 + bright * 0.3 + rng.range(-0.3, 0.4)),
        fill: rng.pickWeighted(FILLS, [
            3,                          // flat
            3,                          // ramp
            1 + intricate * 3,          // hatch
            1 + noisy * 2.5,            // stipple
            1 + bright * 2.5,           // halftone
            1 + (1 - solid) * 2,        // hollow — outline only
        ]),
        hatchAngle: rng.range(0, Math.PI),
        hatchScale: rng.range(40, 160) * (0.6 + intricate * 0.9),
        outline: rng.bool(0.45 + angular * 0.3) ? rng.range(0.3, 1) : 0,
        // Posterisation is a value-structure decision, and it is the cheapest
        // way to make one track look printed and another look lit.
        posterize: rng.bool(0.3) ? rng.int(3, 6) : 0,
    };

    // STAGING: where things go, and how their sizes are distributed.
    //
    // The first four stages separated songs almost entirely on layout while
    // their feature SCALE collapsed — every stage placed similarly-sized
    // elements, so size stopped being a variable at all. A shared lattice fixes
    // both halves: the stages agree with each other about placement, which is
    // what makes a video look like itself, and the size hierarchy becomes a
    // decision the song makes rather than one each stage makes for itself.
    const lattice = {
        kind: rng.pickWeighted(LATTICES, [
            1 + angular * 3,            // grid
            1 + (1 - angular) * 2,      // radial
            1 + (1 - angular) * 2,      // spiral
            2,                          // scatter
            1 + angular * 2,            // strata
        ]),
        jitter: clamp01(rng.range(0, 0.5) + (1 - angular) * 0.3),
        spread: rng.range(0.5, 0.85) + dynamic * 0.35,
        // A few large and many small, or all one size. Busy material earns the
        // hierarchy; a sparse track wants its elements to be equals.
        scaleSpread: clamp01(0.15 + intricate * 0.6 + rng.range(-0.2, 0.25)),
        // Whether the big ones sit in the middle or around the edges.
        scaleBias: rng.range(-1, 1),
        // How big the song's elements are AT ALL — a per-song decision rather
        // than a per-stage one.
        //
        // This is the block the measurements kept pointing at. Sharing a cast
        // lowered the floor as predicted, but `scale` — feature size — came out
        // consistently WORSE than the legacy arm (0.033 against 0.053, well
        // outside the noise), because every stage still chose its own element
        // size from its own param range and every song therefore landed in the
        // same place. A song made of six huge forms and a song made of four
        // hundred tiny ones are different videos before anything else is
        // decided; that decision belongs here.
        elementScale: 0.35 * 2 ** rng.range(-1.4, 1.4) * (1.25 - intricate * 0.5),

        // FOCUS: one to three points the field is disturbed around.
        //
        // A full-frame texture has no composition — measured, its layout
        // distance between two songs is 0.004, which is nothing, because
        // edge-to-edge content sits in the same place however its cells fall.
        // That is not the metric failing; there genuinely is nothing to compose.
        // A focal point gives the field somewhere to be about, and because the
        // point moves with the song it gives the layout something to say.
        focusCount: rng.pickWeighted([1, 2, 3], [4, 2, 1]),
        focusRadius: rng.range(0.35, 1.1),
        // Positive draws the field in and densifies it; negative opens a void.
        focusPull: (rng.bool(0.65) ? 1 : -1) * rng.range(0.3, 1),
    };

    return {
        cast: { protagonist, chorus }, ink, lattice,
        character: { angular, intricate, solid },
    };
}

/** Neutral values, so a layer built without an identity renders as it always did. */
export const NEUTRAL_IDENTITY_UNIFORMS = {
    u_castSides: 0, u_castRound: 0.25, u_castElong: 1, u_castTilt: 0,
    u_castNotchN: 0, u_castNotchD: 0, u_castHollow: 0,
    u_chorusSides: 0, u_chorusRound: 0.25, u_chorusElong: 1, u_chorusTilt: 0,
    u_chorusNotchN: 0, u_chorusNotchD: 0, u_chorusHollow: 0,
    u_inkWeight: 0.3, u_inkEdge: 0.5, u_inkFill: 0, u_inkHatchAngle: 0,
    u_inkHatchScale: 80, u_inkOutline: 0, u_inkPosterize: 0,
    u_latKind: 3, u_latJitter: 0.5, u_latSpread: 0.9,
    u_latScaleSpread: 0.3, u_latScaleBias: 0, u_latScale: 0.35,
    u_focusN: 0, u_focusR: 0.6, u_focusPull: 0,
};

/**
 * @param {object} identity
 * @param {object} shape  the personality's signature form
 *
 * The protagonist's GEOMETRY is the signature form, read live rather than
 * copied at generation time. The two were separate decisions in the first
 * draft, which meant a track built on hexagons could have a round protagonist —
 * the signature form said one thing and the thing actually on screen said
 * another, and the shape trait stopped meaning anything for stages. The cast is
 * the signature form made concrete: same sides, same rounding, same tilt, plus
 * the notches and hollows that turn a shape into a character.
 */
export function identityUniforms(identity, shape = null) {
    if (!identity) return { ...NEUTRAL_IDENTITY_UNIFORMS };
    const { protagonist: p, chorus: b } = identity.cast;
    const a = shape ? {
        ...p,
        sides: shape.sides,
        round: shape.roundness,
        elong: shape.elongation,
        tilt: shape.tilt,
    } : p;
    const ink = identity.ink;
    return {
        u_castSides: a.sides, u_castRound: a.round, u_castElong: a.elong, u_castTilt: a.tilt,
        u_castNotchN: a.notchCount, u_castNotchD: a.notchCount ? a.notchDepth : 0,
        u_castHollow: a.hollow,

        // The chorus stays a relative of the protagonist: it inherits the
        // signature form's tilt and usually its sides.
        u_chorusSides: b.sides, u_chorusRound: b.round, u_chorusElong: b.elong,
        u_chorusTilt: a.tilt + b.tiltOffset,
        u_chorusNotchN: b.notchCount, u_chorusNotchD: b.notchCount ? b.notchDepth : 0,
        u_chorusHollow: b.hollow,

        u_inkWeight: ink.weight, u_inkEdge: ink.edge,
        u_inkFill: FILLS.indexOf(ink.fill),
        u_inkHatchAngle: ink.hatchAngle, u_inkHatchScale: ink.hatchScale,
        u_inkOutline: ink.outline, u_inkPosterize: ink.posterize,

        u_latKind: LATTICES.indexOf(identity.lattice.kind),
        u_latJitter: identity.lattice.jitter,
        u_latSpread: identity.lattice.spread,
        u_latScaleSpread: identity.lattice.scaleSpread,
        u_latScaleBias: identity.lattice.scaleBias,
        u_latScale: identity.lattice.elementScale,
        u_focusN: identity.lattice.focusCount,
        u_focusR: identity.lattice.focusRadius,
        u_focusPull: identity.lattice.focusPull,
    };
}

const SHAPE_NAMES = { 0: 'round', 3: 'triangular', 4: 'square', 5: 'pentagonal', 6: 'hexagonal', 8: 'octagonal' };

export function describeIdentity(identity) {
    if (!identity) return 'no identity';
    const { protagonist: a, chorus: b } = identity.cast;
    const form = (m) => `${SHAPE_NAMES[m.sides] || `${m.sides}-sided`}` +
        `${m.notchCount ? `/${m.notchCount}-notch` : ''}${m.hollow ? '/hollow' : ''}`;
    const ink = identity.ink;
    return `cast ${form(a)} + ${form(b)} · ink ${ink.fill}` +
        `${ink.outline ? '+outline' : ''}${ink.posterize ? `/${ink.posterize}-tone` : ''}` +
        ` w${ink.weight.toFixed(2)} · on ${identity.lattice.kind}` +
        ` · ${identity.lattice.focusCount} focus` +
        ` at ${identity.lattice.elementScale < 0.2 ? 'tiny' :
               identity.lattice.elementScale > 0.6 ? 'huge' : 'mid'} scale`;
}
