Source: renderers/renderer.js

import {CONSOLE_GROUP_STYLE, CONSOLE_MESSAGE_STYLE, DEBUG_MODE} from "../global/constants";
import vertexShaderSource from '../shaders/vertexShaderInit.vert';

/**
 * Abstract base class for WebGL-based renderers.
 * Provides common WebGL context initialization, shader compilation,
 * program creation, and basic rendering infrastructure.
 *
 * @abstract
 */
class Renderer {
    /**
     * Creates a new Renderer instance and initializes the WebGL context.
     *
     * @param {HTMLCanvasElement} canvas - The canvas element to render to.
     */
    constructor(canvas) {
        this.canvas = canvas;

        this.gl = this.canvas.getContext("webgl", {
            antialias: false,
            alpha: false,
            depth: false,
            stencil: false,
            preserveDrawingBuffer: false,
            powerPreference: "high-performance",
        });

        if (!this.gl) {
            alert('WebGL is not supported by your browser or crashed.');
            return;
        }

        /** @type {string} Vertex shader source code */
        this.vertexShaderSource = vertexShaderSource;

        /** @type {WebGLShader|null} */
        this.vertexShader = null;
        /** @type {WebGLShader|null} */
        this.fragmentShader = null;
        /** @type {WebGLProgram|null} */
        this.program = null;

        this.onWebGLContextLost = this.onWebGLContextLost.bind(this);
        this.canvas.addEventListener('webglcontextlost', this.onWebGLContextLost);
    }

    /**
     * Handles WebGL context lost events and attempts to recover the context.
     *
     * @param {WebGLContextEvent} event - The context lost event.
     */
    onWebGLContextLost(event) {
        event.preventDefault();
        console.error(
            `%c ${this.constructor.name}: onWebGLContextLost %c WebGL context lost. Attempting to recover...`,
            CONSOLE_GROUP_STYLE,
            CONSOLE_MESSAGE_STYLE
        );
        this.init();
    }

    /**
     * Initializes the renderer. Must be implemented by subclasses.
     *
     * @abstract
     * @throws {Error} Always throws an error if not implemented.
     */
    init() {
        throw new Error('Not implemented');
    }

    /**
     * Compiles shader code.
     * @param {string} source - Shader source code
     * @param {GLenum} type - Shader type (gl.VERTEX_SHADER or gl.FRAGMENT_SHADER)
     * @return {WebGLShader|null} Compiled shader or null on failure
     */
    compileShader(source, type) {
        if (DEBUG_MODE) {
            console.groupCollapsed(`%c ${this.constructor.name}: %c compileShader`, CONSOLE_GROUP_STYLE, CONSOLE_MESSAGE_STYLE);
            console.log(`Shader GLenum type: ${type}\nShader code: ${source}`);
        }

        const shader = this.gl.createShader(type);
        this.gl.shaderSource(shader, source);
        this.gl.compileShader(shader);

        if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
            console.error(this.gl.getShaderInfoLog(shader));
            this.gl.deleteShader(shader);
            if (DEBUG_MODE) console.groupEnd();
            return null;
        }
        if (DEBUG_MODE) console.groupEnd();

        return shader;
    }

    /**
     * Creates the fragment shader source code.
     * Must be implemented by subclasses to define their specific shader.
     *
     * @abstract
     * @returns {string} Fragment shader source code
     * @throws {Error} Always throws an error if not implemented.
     */
    createFragmentShaderSource() {
        throw new Error('The createFragmentShaderSource method must be implemented in child classes');
    }

    /**
     * Initializes WebGL program, shaders, and full-screen quad.
     * Compiles vertex and fragment shaders, links the program,
     * and sets up the position attribute buffer.
     */
    initGLProgram() {
        if (DEBUG_MODE) console.groupCollapsed(`%c ${this.constructor.name}:%c initGLProgram`, CONSOLE_GROUP_STYLE, CONSOLE_MESSAGE_STYLE);

        if (this.program) this.gl.deleteProgram(this.program);
        if (this.fragmentShader) this.gl.deleteShader(this.fragmentShader);

        if (!this.vertexShader) {
            this.vertexShader = this.compileShader(this.vertexShaderSource, this.gl.VERTEX_SHADER);
        }
        this.fragmentShader = this.compileShader(this.createFragmentShaderSource(), this.gl.FRAGMENT_SHADER);

        this.program = this.gl.createProgram();
        this.gl.attachShader(this.program, this.vertexShader);
        this.gl.attachShader(this.program, this.fragmentShader);
        this.gl.linkProgram(this.program);

        if (!this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS)) {
            console.error(this.gl.getProgramInfoLog(this.program));
        }
        this.gl.useProgram(this.program);

        // Full-screen quad
        const positionBuffer = this.gl.createBuffer();
        this.gl.bindBuffer(this.gl.ARRAY_BUFFER, positionBuffer);
        const positions = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
        this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW);
        const positionLoc = this.gl.getAttribLocation(this.program, "a_position");
        this.gl.enableVertexAttribArray(positionLoc);
        this.gl.vertexAttribPointer(positionLoc, 2, this.gl.FLOAT, false, 0, 0);

        // Hook for subclasses to cache uniform locations
        this.onProgramCreated();

        if (DEBUG_MODE) console.groupEnd();
    }

    /**
     * Hook called after GL program is created and linked.
     * Subclasses can override to cache uniform locations.
     * Default implementation does nothing.
     */
    onProgramCreated() {
        // Empty by default - subclasses override
    }

    /**
     * Base draw operation: sets viewport, clears the canvas, and draws the quad.
     * Subclasses should call this after uploading their uniforms.
     */
    baseDraw() {
        const w = this.canvas.width;
        const h = this.canvas.height;

        this.gl.viewport(0, 0, w, h);
        this.gl.clearColor(0, 0, 0, 1);
        this.gl.clear(this.gl.COLOR_BUFFER_BIT);
        this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
    }

    /**
     * Performs the rendering operation. Must be implemented by subclasses.
     *
     * @abstract
     * @throws {Error} Always throws an error if not implemented.
     */
    draw() {
        throw new Error('Not implemented');
    }

    /**
     * Resets the renderer to its initial state. Must be implemented by subclasses.
     *
     * @abstract
     * @throws {Error} Always throws an error if not implemented.
     */
    reset() {
        throw new Error('Not implemented');
    }

    /**
     * Cleans up renderer resources and releases references.
     */
    destroy() {
        // Clean up WebGL resources
        if (this.program) {
            this.gl.deleteProgram(this.program);
            this.program = null;
        }
        if (this.vertexShader) {
            this.gl.deleteShader(this.vertexShader);
            this.vertexShader = null;
        }
        if (this.fragmentShader) {
            this.gl.deleteShader(this.fragmentShader);
            this.fragmentShader = null;
        }

        this.gl = null;
        this.canvas = null;
    }
}

export default Renderer;