// 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.

/**
 * Solids a 3D cast member is assembled from, as the part's kind index.
 *
 * `prism` is the 2D cast profile extruded — the protagonist given a body — and
 * it is deliberately first, because a form built only from library primitives
 * would be a shape the song did not choose. The other four are what a profile
 * cannot be: something that bulges, something that tapers, something with a
 * hole you can see through from an angle.
 */
export const SOLIDS = ['prism', 'box', 'capsule', 'torus', 'sphere'];

/**
 * How the parts repeat, as the shader's `u_formSym` index.
 *
 * This is the load-bearing half of the assembly. Parts unioned at random
 * positions read as debris; the same parts under a symmetry read as DESIGNED,
 * and a designed object is the only kind worth calling a protagonist. It is
 * also what keeps the form recognisable from any angle, which is the whole
 * reason for giving it a third dimension.
 */
export const SYMMETRIES = ['none', 'mirror', 'radial', 'stack'];

/** Booleans a part can join with, as the part's op index. */
export const FORM_OPS = ['union', 'blend', 'carve'];

/** How many parts an assembly can have. The shader loops to exactly this. */
export const MAX_FORM_PARTS = 6;

/** 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'];

/**
 * How the song's subject IMPACTS a field, as the shader's `u_impact` index.
 *
 * A pattern spread evenly over the screen has nothing to watch: no subject, so
 * nowhere for the eye to rest and no way to tell one song's version from
 * another's. Warping the field around a focal point was the first attempt and it
 * changed the cells without changing where the frame's energy was, which is the
 * same thing as still having no subject.
 *
 * So the subject is a form that does something TO the field, and which thing is
 * the song's decision — the same form punching a hole, bending the pattern, or
 * running it at a different rate are three different videos.
 */
export const IMPACTS = ['shift', 'warp', 'punch', 'morph', 'overlay'];

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,
    };
}

/**
 * The protagonist as a SOLID: a small assembly of parts, joined by booleans
 * under a symmetry.
 *
 * The 2D cast is a silhouette, and a silhouette is the same picture from every
 * angle — which means a scene that turns one is not showing you anything new,
 * it is showing you the same outline rotated. That is the ceiling this exists
 * to lift: an assembly's outline CHANGES as it turns, so a shot of it has
 * somewhere to go over eight bars without the scene inventing motion.
 *
 * It is content rather than a modifier by the EPIC-3 §4 test: a stage handed a
 * default assembly draws a plain extruded profile, and no stage could have
 * invented "a five-fold radial of carved prisms with a torus through it" for
 * itself. The parts stay tied to the 2D cast — `prism` parts ARE the song's
 * profile — so the solid and the silhouette are the same character rather than
 * two unrelated generators running side by side.
 */
function generateForm(rng, { angular, intricate, solid }) {
    const count = rng.pickWeighted([2, 3, 4, 5, 6],
        [3, 3 + intricate, 1.5 + intricate * 3, 0.5 + intricate * 3, 0.2 + intricate * 2]);

    const symmetry = rng.pickWeighted(SYMMETRIES, [
        1,                          // none — rare, and it shows
        2 + angular,                // mirror
        2 + (1 - angular) * 2,      // radial
        1 + angular * 1.5,          // stack
    ]);
    // A radial fold of 2 is a mirror by another name, and a stack of 6 is a
    // column rather than an object, so the two symmetries want different counts.
    const symmetryN = symmetry === 'radial' ? rng.int(3, 8) : rng.int(2, 5);

    const parts = [];
    for (let i = 0; i < count; i++) {
        // The first part is the body and always positive: an assembly whose
        // opening move is a subtraction has nothing to subtract from.
        const op = i === 0 ? 'union' : rng.pickWeighted(FORM_OPS, [
            2,                          // union
            1 + (1 - angular) * 2.5,    // blend — smooth, and the soft look
            0.6 + intricate * 2,        // carve — holes, and the made look
        ]);
        const kind = rng.pickWeighted(SOLIDS, [
            // The profile leads, so the solid keeps the song's own outline.
            4,
            1 + angular * 2,            // box
            1 + (1 - angular) * 1.5,    // capsule
            0.6 + intricate * 1.6,      // torus
            1 + (1 - angular),          // sphere
        ]);
        // Parts near the origin build a body; parts far out build limbs. The
        // first one is centred so there is always something at the middle.
        const reach = i === 0 ? 0 : rng.range(0.15, 0.85) * (0.6 + intricate * 0.7);
        const dir = rng.range(0, Math.PI * 2);
        const size = (i === 0 ? rng.range(0.55, 0.95) : rng.range(0.2, 0.6))
            * (1.15 - intricate * 0.35);
        parts.push({
            kind, op,
            offset: [Math.cos(dir) * reach, rng.range(-0.7, 0.7) * reach, Math.sin(dir) * reach],
            scale: [size, size * rng.range(0.6, 1.5), size * rng.range(0.35, 1.2)],
            yaw: rng.range(0, Math.PI * 2),
            pitch: rng.range(-0.9, 0.9),
            // Rounding the part's own surface, on top of the ink's edge. A
            // solid track wants blunt parts; a dynamic one wants sharp ones.
            round: rng.range(0, 0.35) * (0.4 + solid * 1.2),
        });
    }

    return {
        parts, symmetry, symmetryN,
        // THE CHORUS SOLID: the protagonist's body plan, simplified.
        //
        // A relative rather than a stranger, for the same reason the 2D chorus
        // is: a frame full of both has to read as one production. So it is the
        // first few parts of the same assembly under its own symmetry and its
        // own proportions — which is what a supporting character IS, structurally.
        //
        // It costs no extra part rows. A second full assembly would have doubled
        // the uniform array for something that must not look like a different
        // object anyway, and "fewer parts, squashed differently" is both cheaper
        // and a better description of the thing.
        chorus: {
            count: Math.min(parts.length, rng.int(1, 3)),
            symmetry: rng.bool(0.55) ? symmetry : rng.pick(['none', 'mirror']),
            symmetryN: rng.int(2, 5),
            // Squashed and thinned against the protagonist. A chorus that is
            // merely a smaller protagonist adds numbers and no information.
            flat: rng.range(0.5, 1.7),
            thin: rng.range(0.45, 1.5),
        },
        // How far a `blend` op melts two parts into one. Low reads as welded
        // hard edges, high as a single lump — both are legible, and the middle
        // is where an assembly stops looking like parts at all.
        blend: rng.range(0.04, 0.3) * (1.4 - angular * 0.8),
        // How deep the solid is relative to how wide. A track can be built on
        // slabs or on columns, and that decision is visible before anything else.
        depth: rng.range(0.45, 1.6),
    };
}

/**
 * @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),
        impact: rng.pick(IMPACTS),
    };

    // The solid the protagonist is, as opposed to the outline it casts. Forked
    // rather than drawn inline so adding it does not shift every decision made
    // after it — an identity generated today has to stay the identity it was.
    const form = generateForm(rng.fork('form'), { angular, intricate, solid });

    return {
        cast: { protagonist, chorus }, ink, lattice, form,
        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, u_impact: 0,

    // A count of zero is the shader's instruction to fall back to the 2D cast
    // extruded, so a scene that marches the solid still draws the right
    // character when it is handed no identity at all.
    u_formCount: 0, u_formSym: 0, u_formSymN: 3, u_formBlend: 0.12, u_formDepth: 0.8,
    u_formChorusN: 0, u_formChorusSym: 0, u_formChorusSymN: 3,
    u_formChorusFlat: 1, u_formChorusThin: 1,
    u_formPart: Array.from({ length: MAX_FORM_PARTS * 3 }, () => [0, 0, 0, 0]),
};

/**
 * The assembly, packed for the shader: three vec4 per part.
 *
 * A part is eleven numbers, and eleven scalar uniforms times six parts is
 * sixty-six declarations nobody would keep in step with the generator. Packed
 * rows are indexed by the loop counter instead, which is the one array access
 * GLSL ES 1.0 allows and is why the layout is fixed-width rather than tight.
 *
 *   row 0   offset.xyz              | solid index
 *   row 1   scale.xyz               | boolean op index
 *   row 2   yaw, pitch, round       | unused
 */
function chorusUniforms(chorus) {
    if (!chorus) {
        return {
            u_formChorusN: 0, u_formChorusSym: 0, u_formChorusSymN: 3,
            u_formChorusFlat: 1, u_formChorusThin: 1,
        };
    }
    return {
        u_formChorusN: chorus.count,
        u_formChorusSym: SYMMETRIES.indexOf(chorus.symmetry),
        u_formChorusSymN: chorus.symmetryN,
        u_formChorusFlat: chorus.flat,
        u_formChorusThin: chorus.thin,
    };
}

function formPartRows(form) {
    const rows = [];
    for (let i = 0; i < MAX_FORM_PARTS; i++) {
        const p = form && form.parts[i];
        if (!p) { rows.push([0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]); continue; }
        rows.push([p.offset[0], p.offset[1], p.offset[2], SOLIDS.indexOf(p.kind)]);
        rows.push([p.scale[0], p.scale[1], p.scale[2], FORM_OPS.indexOf(p.op)]);
        rows.push([p.yaw, p.pitch, p.round, 0]);
    }
    return rows;
}

/**
 * @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,
        u_impact: IMPACTS.indexOf(identity.lattice.impact),

        u_formCount: identity.form ? identity.form.parts.length : 0,
        u_formSym: identity.form ? SYMMETRIES.indexOf(identity.form.symmetry) : 0,
        u_formSymN: identity.form ? identity.form.symmetryN : 3,
        u_formBlend: identity.form ? identity.form.blend : 0.12,
        u_formDepth: identity.form ? identity.form.depth : 0.8,
        u_formPart: formPartRows(identity.form),

        // Defaulted rather than assumed. A check harness builds identities by
        // hand to probe a scene, and reading through a missing sub-object here
        // throws inside a uniform getter — where the only symptom is a page that
        // never finishes and never says why. Cost of the guard: nothing.
        ...chorusUniforms(identity.form && identity.form.chorus),
    };
}

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;
    const solid = identity.form
        ? ` · solid ${identity.form.parts.length}-part/${identity.form.symmetry}` +
          `${identity.form.symmetry === 'none' ? '' : identity.form.symmetryN}`
        : '';
    return `cast ${form(a)} + ${form(b)}${solid} · ink ${ink.fill}` +
        `${ink.outline ? '+outline' : ''}${ink.posterize ? `/${ink.posterize}-tone` : ''}` +
        ` w${ink.weight.toFixed(2)} · on ${identity.lattice.kind}` +
        ` · ${identity.lattice.focusCount} focus/${identity.lattice.impact}` +
        ` at ${identity.lattice.elementScale < 0.2 ? 'tiny' :
               identity.lattice.elementScale > 0.6 ? 'huge' : 'mid'} scale`;
}
