Source: Shader.js

import { addSignals } from './Signals.js'
import { Util } from './Util.js'

/**
* @typedef {Object} Shader~Sampler
* A reference to a 2D texture used in the shader.
* @property {number} id - Unique identifier for the sampler
* @property {string} name - Sampler variable name in shader program (e.g., "kd" for diffuse texture)
* @property {string} label - Display label for UI/menus
* @property {Array<Object>} samplers - Array of raster definitions
* @property {number} samplers[].id - Raster identifier
* @property {string} samplers[].type - Raster type (e.g., 'color', 'normal')
* @property {boolean} [samplers[].bind=true] - Whether sampler should be bound in prepareGL
* @property {boolean} [samplers[].load=true] - Whether sampler should load from raster
* @property {Array<Object>} uniforms - Shader uniform variables
* @property {string} uniforms[].type - Data type ('vec4'|'vec3'|'vec2'|'float'|'int')
* @property {boolean} uniforms[].needsUpdate - Whether uniform needs GPU update
* @property {number} uniforms[].value - Uniform value or array of values
*/

/**
 * Shader module provides WebGL shader program management for OpenLIME.
 * Supports both WebGL 1.0 and 2.0/3.0 GLSL specifications with automatic version detection.
 * 
 * Shader class manages WebGL shader programs.
 * Features:
 * - GLSL/ES 2.0 and 3.0 support
 * - Automatic uniform management
 * - Multiple shader modes
 * - Filter pipeline
 * - Automatic version selection
 */
class Shader {
	/**
	 * Creates a new Shader instance.
	 * @param {Object} [options] - Configuration options
	 * @param {Array<Shader~Sampler>} [options.samplers=[]] - Texture sampler definitions
	 * @param {Object.<string,Object>} [options.uniforms={}] - Shader uniform variables
	 * @param {string} [options.label=null] - Display label for the shader
	 * @param {Array<string>} [options.modes=[]] - Available shader modes
	 * @param {number} [options.version=100] - GLSL version (100 for WebGL1, 300 for WebGL2)
	 * @param {boolean} [options.debug=false] - Enable debug output
	 * @fires Shader#update
	 */
	constructor(options) {
		Object.assign(this, {
			version: 100,   //check for webglversion. 
			debug: false,
			samplers: [],
			uniforms: {},
			label: null,
			program: null,      //webgl program
			modes: [],
			mode: null, // The current mode
			needsUpdate: true,
			tileSize: [0, 0]
		});
		addSignals(Shader, 'update');
		Object.assign(this, options);
		this.filters = [];
	}

	/**
	 * Clears all filters from the shader pipeline.
	 * @fires Shader#update
	 */
	clearFilters() {
		this.filters = [];
		this.needsUpdate = true;
		this.emit('update');
	}

	/**
	 * Adds a filter to the shader pipeline.
	 * @param {Object} filter - Filter to add
	 * @fires Shader#update
	 */
	addFilter(f) {
		f.shader = this;
		this.filters.push(f);
		this.needsUpdate = true;
		f.needsUpdate = true;
		this.emit('update');
	}

	/**
	 * Removes a filter from the pipeline by name.
	 * @param {string} name - Name of filter to remove
	 * @fires Shader#update
	 */
	removeFilter(name) {
		this.filters = this.filters.filter((v) => {
			return v.name != name;
		});
		this.needsUpdate = true;
		this.emit('update');
	}

	/**
	 * Sets the current shader mode.
	 * @param {string} mode - Mode identifier (must be in options.modes)
	 * @throws {Error} If mode is not recognized
	 */
	setMode(mode) {
		if (this.modes.indexOf(mode) == -1)
			throw Error("Unknown mode: " + mode);
		this.mode = mode;
		this.needsUpdate = true;
	}

	/**
	 * Restores WebGL state after context loss.
	 * @param {WebGLRenderingContext} gl - WebGL context
	 * @private
	 */
	restoreWebGL(gl) {
		this.createProgram(gl);
	}

	/**
	 * Sets tile dimensions for shader calculations.
	 * @param {number[]} size - [width, height] of tile
	 */
	setTileSize(sz) {
		this.tileSize = sz;
		this.needsUpdate = true;
	}

	/**
	 * Sets a uniform variable value.
	 * @param {string} name - Uniform variable name
	 * @param {number|boolean|Array} value - Value to set
	 * @throws {Error} If uniform doesn't exist
	 * @fires Shader#update
	 */
	setUniform(name, value) {
		/**
		* The event is fired when a uniform shader variable is changed.
		* @event Camera#update
		*/
		let u = this.getUniform(name);
		if (!u)
			throw new Error(`Unknown '${name}'. It is not a registered uniform.`);
		if ((typeof (value) == "number" || typeof (value) == "boolean") && u.value == value)
			return;
		if (Array.isArray(value) && Array.isArray(u.value) && value.length == u.value.length) {
			let equal = true;
			for (let i = 0; i < value.length; i++)
				if (value[i] != u.value[i]) {
					equal = false;
					break;
				}
			if (equal)
				return;
		}

		u.value = value;
		u.needsUpdate = true;
		this.emit('update');
	}

	/** @ignore */
	completeFragShaderSrc(gl) {
		let gl2 = !(gl instanceof WebGLRenderingContext);

		let src = `${gl2 ? '#version 300 es' : ''}\n`;
		src += `precision highp float;\n`;
		src += `precision highp int;\n`;
		src += `const vec2 tileSize = vec2(${this.tileSize[0]}.0, ${this.tileSize[1]}.0);\n`;
		src += this.fragShaderSrc() + '\n';

		for (let f of this.filters) {
			src += `		// Filter: ${f.name}\n`;
			src += f.fragModeSrc() + '\n';
			src += f.fragSamplerSrc() + '\n';
			src += f.fragUniformSrc() + '\n';
			src += f.fragDataSrc() + '\n\n';
		}

		src += `
		${gl2 ? 'out' : ''} vec4 color;
		void main() { 
			color = data();
			`;
		for (let f of this.filters) {
			src += `color=${f.functionName()}(color);\n`
		}
		src += `${gl2 ? '' : 'gl_FragColor = color;'}
		}`;
		return src;
	}

	/**
	 * Creates the WebGL shader program.
	 * @param {WebGLRenderingContext} gl - WebGL context
	 * @private
	 * @throws {Error} If shader compilation or linking fails
	 */
	createProgram(gl) {

		let vert = gl.createShader(gl.VERTEX_SHADER);
		gl.shaderSource(vert, this.vertShaderSrc(gl));

		gl.compileShader(vert);
		let compiled = gl.getShaderParameter(vert, gl.COMPILE_STATUS);
		if (!compiled) {
			Util.printSrcCode(this.vertShaderSrc(gl))
			console.log(gl.getShaderInfoLog(vert));
			throw Error("Failed vertex shader compilation: see console log and ask for support.");
		} else if (this.debug) {
			Util.printSrcCode(this.vertShaderSrc(gl))
		}

		let frag = gl.createShader(gl.FRAGMENT_SHADER);
		gl.shaderSource(frag, this.completeFragShaderSrc(gl));
		gl.compileShader(frag);

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

		let program = gl.createProgram();

		gl.getShaderParameter(frag, gl.COMPILE_STATUS);
		compiled = gl.getShaderParameter(frag, gl.COMPILE_STATUS);
		if (!compiled) {
			Util.printSrcCode(this.completeFragShaderSrc(gl));
			console.log(gl.getShaderInfoLog(frag));
			throw Error("Failed fragment shader compilation: see console log and ask for support.");
		} else if (this.debug) {
			Util.printSrcCode(this.completeFragShaderSrc(gl));
		}
		gl.attachShader(program, vert);
		gl.attachShader(program, frag);
		gl.linkProgram(program);

		if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
			var info = gl.getProgramInfoLog(program);
			throw new Error('Could not compile WebGL program. \n\n' + info);
		}

		//sampler units;
		for (let sampler of this.samplers)
			sampler.location = gl.getUniformLocation(program, sampler.name);

		// filter samplers
		for (let f of this.filters)
			for (let sampler of f.samplers)
				sampler.location = gl.getUniformLocation(program, sampler.name);

		this.coordattrib = gl.getAttribLocation(program, "a_position");
		gl.vertexAttribPointer(this.coordattrib, 3, gl.FLOAT, false, 0, 0);
		gl.enableVertexAttribArray(this.coordattrib);

		this.texattrib = gl.getAttribLocation(program, "a_texcoord");
		gl.vertexAttribPointer(this.texattrib, 2, gl.FLOAT, false, 0, 0);
		gl.enableVertexAttribArray(this.texattrib);

		this.matrixlocation = gl.getUniformLocation(program, "u_matrix");

		this.program = program;
		this.needsUpdate = false;

		for (let uniform of Object.values(this.allUniforms())) {
			uniform.location = null;
			uniform.needsUpdate = true;
		}

		for (let f of this.filters)
			f.prepare(gl);

	}

	/**
	 * Gets a uniform variable by name.
	 * Searches both shader uniforms and filter uniforms.
	 * @param {string} name - Uniform variable name
	 * @returns {Object|undefined} Uniform object if found
	 */
	getUniform(name) {
		let u = this.uniforms[name];
		if (u) return u;
		for (let f of this.filters) {
			u = f.uniforms[name];
			if (u) return u;
		}
		return u;
	}

	/**
	 * Returns all uniform variables associated with the shader and its filters.
	 * Combines uniforms from both the base shader and all active filters into a single object.
	 * @returns {Object.<string, Object>} Combined uniform variables
	 */
	allUniforms() {
		const result = this.uniforms;
		for (let f of this.filters) {
			Object.assign(result, f.uniforms);
		}
		return result;
	}

	/**
	 * Updates all uniform values in the GPU.
	 * @param {WebGLRenderingContext} gl - WebGL context
	 * @private
	 */
	updateUniforms(gl) {
		for (const [name, uniform] of Object.entries(this.allUniforms())) {
			if (!uniform.location)
				uniform.location = gl.getUniformLocation(this.program, name);

			if (!uniform.location)  //uniform not used in program
				continue;

			if (uniform.needsUpdate) {
				let value = uniform.value;
				switch (uniform.type) {
					case 'vec4': gl.uniform4fv(uniform.location, value); break;
					case 'vec3': gl.uniform3fv(uniform.location, value); break;
					case 'vec2': gl.uniform2fv(uniform.location, value); break;
					case 'float': gl.uniform1f(uniform.location, value); break;
					case 'int': gl.uniform1i(uniform.location, value); break;
					case 'bool': gl.uniform1i(uniform.location, value); break;
					case 'mat3': gl.uniformMatrix3fv(uniform.location, false, value); break;
					case 'mat4': gl.uniformMatrix4fv(uniform.location, false, value); break;
					default: throw Error('Unknown uniform type: ' + u.type);
				}
				uniform.needsUpdate = false;
			}
		}
	}

	/**
	 * Gets vertex shader source code.
	 * Default implementation provides basic vertex transformation and texture coordinate passing.
	 * @param {WebGLRenderingContext} gl - WebGL context
	 * @returns {string} Vertex shader source code
	 */
	vertShaderSrc(gl) {
		let gl2 = !(gl instanceof WebGLRenderingContext);
		return `${gl2 ? '#version 300 es' : ''}

precision highp float; 
precision highp int; 

uniform mat4 u_matrix;
${gl2 ? 'in' : 'attribute'} vec4 a_position;
${gl2 ? 'in' : 'attribute'} vec2 a_texcoord;

${gl2 ? 'out' : 'varying'} vec2 v_texcoord;

			void main() {
				gl_Position = u_matrix * a_position;
				v_texcoord = a_texcoord;
			} `;
	}

	/**
	 * Gets fragment shader source code.
	 * Must be overridden in derived classes for custom shading.
	 * @param {WebGLRenderingContext} gl - WebGL context
	 * @returns {string} Fragment shader source code
	 * @virtual
	 */
	fragShaderSrc(gl) {
		let gl2 = !(gl instanceof WebGLRenderingContext);
		let str = `

uniform sampler2D kd;

${gl2 ? 'in' : 'varying'} vec2 v_texcoord;

vec4 data() {
	return texture${gl2 ? '' : '2D'}(kd, v_texcoord);
}
`;
		return str;
	}
}
/**
 * Fired when shader state changes (uniforms, filters, etc).
 * @event Shader#update
 */

export { Shader }