import * as THREE from 'three';
import { VERTEX_SHADER, PREAMBLE } from './shader-contract.js';
import { Layer, buildShaderUniforms, setFrameUniforms } from './Layer.js';

//
// OSD — the on-screen title plate.
//
// A small music-video plate that sits in the corner and names the track. It is
// NOT a DOM overlay: it is rendered into the same layer stack an export runs,
// so the title appears in the exported video exactly where it shows in preview.
//
// Following the personality is not optional fat, it is the point. The plate is
// built out of the same four traits every scene is:
//
//   shape   — the track's signature form is stamped beside the title as a
//             monogram, so the plate carries the same subject the video does.
//   camera  — the same operator holds the plate: it drifts and sways and takes
//             the bar-locked breath every scene takes.
//   space   — a soft wash of the location's air sits behind it so it stays
//             readable over a busy scene.
//   style   — the title is edged in the track's line weight and softness, folds
//             nothing, and carries the same surface grain.
//
// And it is reactive rather than inert: the plate's glow rides the loudness and
// it breathes on the bar, so even a static corner moves with the music.
//
// DETERMINISM: the plate's glyph mask is rasterised once off-frame and held as
// a texture. It uses no clock, no Math.random, no wall time — the same seed
// always produces the same title plate, and preview and export share the one
// texture, so they cannot disagree.

/**
 * The corner plate, a fullscreen pass like any other layer so it slots into the
 * compositor's blend loop. It masks to a block in the bottom-left corner.
 *
 * "OSD" follows the personality because it shares the Layer contract: the same
 * palette, audio features and signature uniforms as every scene, driven by the
 * same per-frame pass.
 */
export class OSDLayer extends Layer {
    constructor(renderer, { text = '', personality = null, palette = [] } = {}) {
        // A layer with no rendered params; it is its own document, not a scene.
        // opacity is 0 until a title exists: the compositor skips zero-opacity
        // layers before they draw, so a nameless plate never writes over a frame.
        super({ module: { name: 'OSD title' }, blend: 'normal', opacity: 0 });
        this.renderer = renderer;
        this.personality = personality;
        this.palette = palette;
        this.text = '';
        this._titleTex = null;

        this.uniforms = buildShaderUniforms({ params: {} }, {}, 0);
        this.uniforms.u_title = { value: null };
        this.uniforms.u_titleAspect = { value: 1 };

        this.material = new THREE.ShaderMaterial({
            vertexShader: VERTEX_SHADER,
            fragmentShader: PREAMBLE + `\nuniform sampler2D u_title;\nuniform float u_titleAspect;\n${OSD_FRAGMENT}`,
            uniforms: this.uniforms,
            depthTest: false,
            depthWrite: false,
        });

        this.setText(text, personality, palette);
    }

    /** Set the track name (and refresh personality/palette it renders with). */
    setText(text, personality = null, palette = null) {
        this.text = text || '';
        if (personality) this.personality = personality;
        if (palette) this.palette = palette;
        this._rebuildTitleTex();
        return this;
    }

    setPersonality(personality) { this.personality = personality; this._rebuildTitleTex(); return this; }
    setPalette(palette) {
        // Palette lives in uniforms (setFrameUniforms), not the texture, so a
        // recolour needs no re-raster. Keep the reference for parity anyway.
        this.palette = palette;
        return this;
    }

    /** Rasterise the song name once, off-frame. See module notes on determinism. */
    _rebuildTitleTex() {
        if (this._titleTex) { this._titleTex.dispose(); this._titleTex = null; }
        const canvas = renderTitlePlate(this.text, this.personality);
        this.uniforms.u_title.value = null;
        this.opacity = canvas ? 1 : 0;
        if (!canvas) return;
        const tex = new THREE.CanvasTexture(canvas);
        tex.colorSpace = THREE.SRGBColorSpace;
        this.uniforms.u_title.value = tex;
        this.uniforms.u_titleAspect.value = canvas.width / canvas.height;
        this._titleTex = tex;
    }

    render(renderer, target, ctx) {
        if (!this.uniforms.u_title.value) return;
        setFrameUniforms(this, renderer, target, ctx);
        renderer.blit(this.material, target);
    }

    dispose() {
        if (this.material) this.material.dispose();
        if (this._titleTex) this._titleTex.dispose();
    }
}

/**
 * Rasterise the title as a white-on-transparent mask the shader recolours.
 * Sized to the glyph run, so the texture's aspect is the plate's aspect.
 * Returns a canvas, or null if there is nothing to draw.
 */
function renderTitlePlate(text, personality) {
    const label = (text || '').trim().toUpperCase();
    if (!label) return null;

    // Letterspacing reflects the track's line weight; a heavier art direction
    // spreads the type further apart.
    const spacing = (personality && personality.style ? personality.style.lineWeight : 0.6) * 14;
    const font = (px) => `700 ${px}px "Arial Black", "Avenir Next", "Helvetica Neue", "Segoe UI", sans-serif`;
    const widthCap = 2048;

    let canvas = document.createElement('canvas');
    let ctx = canvas.getContext('2d');
    const measure = (px) => {
        ctx.font = font(px);
        try { ctx.letterSpacing = `${spacing}px`; } catch { /* older engines ignore it */ }
        return ctx.measureText(label).width;
    };

    let px = 1500;
    let width = measure(px);
    while (width > widthCap && px > 260) { px *= 0.8; width = measure(px); }

    const pad = Math.round(px * 0.06);
    canvas.width = Math.ceil(width + pad * 2);
    canvas.height = Math.ceil(px + pad * 2);

    ctx = canvas.getContext('2d');
    ctx.fillStyle = '#fff';
    ctx.font = font(px);
    try { ctx.letterSpacing = `${spacing}px`; } catch { /* older engines */}
    ctx.textBaseline = 'middle';
    ctx.textAlign = 'center';
    ctx.fillText(label, canvas.width / 2, canvas.height / 2);
    return canvas;
}

// The plate pass. Works in centred aspect-corrected coordinates same as every
// scene, and reads the personality and audio uniforms the contract provides.
const OSD_FRAGMENT = `
void main() {
    vec2 uv = vUv;
    vec2 p = (uv - 0.5) * 2.0;
    p.x *= u_aspect;

    // --- the operator films the plate too: sway, and the bar-locked breath ---
    vec2 cam = vec2(
        sin(u_time * u_sigSwayRate) * u_sigSway,
        cos(u_time * u_sigSwayRate * 0.83) * u_sigSway
    ) * 0.08;
    float breath = 1.0 + u_sigBreathe * sin(u_barPhase * 6.28318530718);

    vec2 corner = vec2(-u_aspect + 0.3, -1.0 + 0.1) + cam;
    float blockH = 0.09 * breath;
    float blockW = blockH * u_titleAspect;
    vec2 blockBL = corner;
    vec2 blockTR = corner + vec2(blockW, blockH);
    vec2 blockC  = (blockBL + blockTR) * 0.5;

    vec3 acc = vec3(0.0);
    float alpha = 0.0;

    // --- space: the location's backing wash, soft, so the plate reads ---
    vec2 rel = (p - blockC) / vec2(blockW + blockH, blockH * 2.2);
    float back = exp(-dot(rel, rel) * 4.0) * 0.32;
    acc += pal(0) * back * 0.4;
    alpha += back;

    // --- shape: the signature form stamped as a monogram left of the title ---
    float emD = blockH * 0.95;
    vec2 emC = vec2(blockBL.x - blockH * 1.15, blockC.y);
    float d = sigShape((p - emC) / max(emD * 0.5, 1e-3)) * (emD * 0.5);
    float emFill = smoothstep(u_sigSoft * emD * 0.15, -u_sigSoft * emD * 0.15, d);
    float emEdge = sigEdge(d);
    vec3 emCol = mix(pal(2), pal(1), 0.35);
    acc += emCol * emFill * 0.2 + emCol * emEdge;
    alpha += emFill * 0.2 + emEdge;

    // --- the title itself, recoloured by the track ---
    vec2 tuv = (p - blockBL) / vec2(blockW, blockH);
    if (tuv.x >= 0.0 && tuv.x <= 1.0 && tuv.y >= 0.0 && tuv.y <= 1.0) {
        float mask = texture2D(u_title, tuv).a;
        float core = smoothstep(0.5, 0.62, mask);
        // A soft glow instead of a hard outline: offset-sampled outlines break
        // into a dotted line around the glyphs once the plate is downsampled.
        float fringe = smoothstep(0.06, 0.5, mask) * (1.0 - core);
        float live = 0.55 + u_loudness * 0.6;

        vec3 fill = pal(1);
        vec3 glow = pal(0);
        acc += fill * core + glow * fringe * 0.55 * live;
        alpha += core + fringe * 0.55 * live;
    }

    // --- the same surface grain every scene carries ---
    acc += sigGrain(uv) * 0.3;

    gl_FragColor = vec4(acc, min(alpha, 1.0) * u_opacity);
}
`;