import * as THREE from 'three';
import { VERTEX_SHADER } from './shader-contract.js';

/**
 * Thin wrapper over WebGLRenderer that provides the two primitives everything
 * else is built from: allocate a render target, and run a fullscreen shader pass
 * into one. Keeping this small matters — preview and export share it exactly,
 * and any state that leaks between frames here would break determinism.
 */
export class Renderer {
    constructor({ width = 1280, height = 720, canvas = null } = {}) {
        this.gl = new THREE.WebGLRenderer({
            canvas: canvas || undefined,
            antialias: false,          // we render through targets; MSAA here buys nothing
            preserveDrawingBuffer: true, // required to read pixels back for hashing/export
            powerPreference: 'high-performance',
        });
        this.gl.autoClear = false;
        this.gl.setPixelRatio(1);      // never device-dependent: output size is explicit
        this.gl.setSize(width, height, false);

        this.width = width;
        this.height = height;

        // Fullscreen quad rig, reused for every pass.
        this.quadScene = new THREE.Scene();
        this.quadCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
        this.quadGeometry = new THREE.PlaneGeometry(2, 2);
        this.quadMesh = new THREE.Mesh(this.quadGeometry, new THREE.MeshBasicMaterial());
        this.quadMesh.frustumCulled = false;
        this.quadScene.add(this.quadMesh);

        this._readBuffer = null;
    }

    get canvas() {
        return this.gl.domElement;
    }

    setSize(width, height) {
        if (width === this.width && height === this.height) return;
        this.width = width;
        this.height = height;
        this.gl.setSize(width, height, false);
        this._readBuffer = null;
    }

    createTarget(width = this.width, height = this.height, options = {}) {
        const target = new THREE.WebGLRenderTarget(width, height, {
            minFilter: THREE.LinearFilter,
            magFilter: THREE.LinearFilter,
            format: THREE.RGBAFormat,
            type: options.float ? THREE.HalfFloatType : THREE.UnsignedByteType,
            depthBuffer: options.depth === true,
            stencilBuffer: false,
            generateMipmaps: false,
        });
        target.texture.wrapS = THREE.ClampToEdgeWrapping;
        target.texture.wrapT = THREE.ClampToEdgeWrapping;
        // Deterministic initial contents: never inherit whatever was in GPU memory.
        this.clear(target);
        return target;
    }

    clear(target = null, r = 0, g = 0, b = 0, a = 1) {
        const prev = this.gl.getClearColor(new THREE.Color());
        const prevAlpha = this.gl.getClearAlpha();
        this.gl.setRenderTarget(target);
        this.gl.setClearColor(new THREE.Color(r, g, b), a);
        this.gl.clear(true, true, true);
        this.gl.setClearColor(prev, prevAlpha);
        this.gl.setRenderTarget(null);
    }

    /** Run a fullscreen shader pass. target === null renders to the canvas. */
    blit(material, target = null) {
        this.quadMesh.material = material;
        this.gl.setRenderTarget(target);
        this.gl.clear(true, false, false);
        this.gl.render(this.quadScene, this.quadCamera);
        this.gl.setRenderTarget(null);
    }

    /** Render a real three.js scene (used by 3D layers). */
    renderScene(scene, camera, target = null, clear = true) {
        this.gl.setRenderTarget(target);
        if (clear) this.gl.clear(true, true, true);
        this.gl.render(scene, camera);
        this.gl.setRenderTarget(null);
    }

    /**
     * Force a material's shader program to compile and link NOW.
     *
     * three.js links through KHR_parallel_shader_compile, so a freshly created
     * material can be drawn with a program that is not ready yet, producing wrong
     * frames until it is. Rendering a throwaway frame and reading it back does not
     * reliably wait for the link; WebGLRenderer.compile() does.
     */
    compileMaterial(material) {
        const previous = this.quadMesh.material;
        this.quadMesh.material = material;
        this.gl.compile(this.quadScene, this.quadCamera);
        this.quadMesh.material = previous;
    }

    /** Same, for a 3D layer's own scene. */
    compileScene(scene, camera) {
        this.gl.compile(scene, camera);
    }

    readPixels(target) {
        const w = target ? target.width : this.width;
        const h = target ? target.height : this.height;
        const needed = w * h * 4;
        if (!this._readBuffer || this._readBuffer.length !== needed) {
            this._readBuffer = new Uint8Array(needed);
        }
        this.gl.readRenderTargetPixels(target, 0, 0, w, h, this._readBuffer);
        return this._readBuffer;
    }

    dispose() {
        this.quadGeometry.dispose();
        this.gl.dispose();
    }
}

/** Convenience for building the shader materials used by passes. */
export function makePassMaterial(fragmentShader, uniforms) {
    return new THREE.ShaderMaterial({
        vertexShader: VERTEX_SHADER,
        fragmentShader,
        uniforms,
        depthTest: false,
        depthWrite: false,
        transparent: true,
    });
}
