import primitiveFragment from "./../shaders/primitive.frag";
import primitiveVertex from "./../shaders/primitive.vert";
import { Batcher } from "./batcher.js";

/**
 * additional import for TypeScript
 * @import {Point} from "./../../../geometries/point.ts";
 */

/**
 * A WebGL Compositor object. This class handles all of the WebGL state<br>
 * Pushes texture regions or shape geometry into WebGL buffers, automatically flushes to GPU
 * @category Rendering
 */
export default class PrimitiveBatcher extends Batcher {
	/**
	 * Initialize the compositor
	 * @ignore
	 */
	init(renderer) {
		super.init(renderer, {
			attributes: [
				{
					// vec3: (x, y, z). z carries `renderable.depth` for
					// perspective projection (Camera3d). Stride = 24 bytes.
					name: "aVertex",
					size: 3,
					type: renderer.gl.FLOAT,
					normalized: false,
					offset: 0 * Float32Array.BYTES_PER_ELEMENT,
				},
				{
					name: "aNormal",
					size: 2,
					type: renderer.gl.FLOAT,
					normalized: false,
					offset: 3 * Float32Array.BYTES_PER_ELEMENT,
				},
				{
					name: "aColor",
					size: 4,
					type: renderer.gl.UNSIGNED_BYTE,
					normalized: true,
					offset: 5 * Float32Array.BYTES_PER_ELEMENT,
				},
			],
			shader: {
				vertex: primitiveVertex,
				fragment: primitiveFragment,
			},
		});

		/**
		 * the current line width applied to the shader uniform
		 * @ignore
		 */
		this.currentLineWidth = 1;
	}

	/**
	 * called by the WebGL renderer when a compositor become the current one
	 */
	bind() {
		const shaderChanged =
			this.renderer.currentProgram !== this.defaultShader.program;
		super.bind();
		if (shaderChanged) {
			// set the default line width uniform after a shader change
			this.currentLineWidth = this.renderer.lineWidth;
			this.currentShader.setUniform("uLineWidth", this.currentLineWidth);
		}
	}

	/**
	 * Reset compositor internal state
	 * @ignore
	 */
	reset() {
		super.reset();
		this.currentLineWidth = 1;
	}

	/**
	 * Draw an array of vertices
	 * @param {GLenum} mode - primitive type to render (gl.POINTS, gl.LINE_STRIP, gl.LINE_LOOP, gl.LINES, gl.TRIANGLE_STRIP, gl.TRIANGLE_FAN, gl.TRIANGLES)
	 * @param {Point[]} verts - an array of vertices
	 * @param {number} [vertexCount=verts.length] - amount of points defined in the points array
	 */
	drawVertices(mode, verts, vertexCount = verts.length) {
		const lineWidth = this.renderer.lineWidth;

		// update uLineWidth uniform if changed
		if (lineWidth !== this.currentLineWidth) {
			this.flush();
			this.currentLineWidth = lineWidth;
			this.currentShader.setUniform("uLineWidth", lineWidth);
		}

		// expand thick lines to triangles with normals for shader-based expansion
		if (mode === this.gl.LINES && lineWidth > 1) {
			this.#expandLinesToTriangles(verts, vertexCount);
			return;
		}

		const viewMatrix = this.viewMatrix;
		const vertexData = this.vertexData;
		const alpha = this.renderer.getGlobalAlpha();
		const colorUint32 = this.renderer.currentColor.toUint32(alpha);
		// z = current renderer depth (Renderable.preDraw); a no-op under ortho,
		// consumed by perspective (Camera3d). Same value for every vertex —
		// primitives don't have per-vertex depth.
		const z = this.renderer.currentDepth;

		if (vertexData.isFull(vertexCount)) {
			// is the vertex buffer full if we add more vertices
			this.flush();
		}

		// flush if drawing vertices with a different drawing mode
		if (mode !== this.mode) {
			this.flush(this.mode);
			this.mode = mode;
		}

		if (!viewMatrix.isIdentity()) {
			// Full 3D transform including the z column (m[8] / m[9] /
			// m[10] / m[14]) so Camera3d's view matrix (X/Y-axis
			// rotation) actually rotates the primitive in 3D. For 2D
			// matrices those slots are identity, so output (x, y, z)
			// is bit-identical to the legacy 2D-only multiply.
			const m = viewMatrix.val;
			for (let i = 0; i < vertexCount; i++) {
				const vert = verts[i];
				const x = vert.x;
				const y = vert.y;
				vertexData.push(
					x * m[0] + y * m[4] + z * m[8] + m[12],
					x * m[1] + y * m[5] + z * m[9] + m[13],
					x * m[2] + y * m[6] + z * m[10] + m[14],
					0,
					0,
					colorUint32,
				);
			}
		} else {
			for (let i = 0; i < vertexCount; i++) {
				const vert = verts[i];
				vertexData.push(vert.x, vert.y, z, 0, 0, colorUint32);
			}
		}

		// force flush for primitive using LINE_STRIP or LINE_LOOP
		if (this.mode === this.gl.LINE_STRIP || this.mode === this.gl.LINE_LOOP) {
			this.flush(this.mode);
		}
	}

	/**
	 * Expand line pairs into triangles with perpendicular normals.
	 * The vertex shader offsets each vertex by aNormal * uLineWidth * 0.5,
	 * producing thick lines without manual geometry expansion in the renderer.
	 * @param {Point[]} verts - line vertices in pairs [from, to, from, to, ...]
	 * @param {number} vertexCount - number of vertices
	 * @ignore
	 */
	#expandLinesToTriangles(verts, vertexCount) {
		const viewMatrix = this.viewMatrix;
		const vertexData = this.vertexData;
		const alpha = this.renderer.getGlobalAlpha();
		const colorUint32 = this.renderer.currentColor.toUint32(alpha);
		const hasTransform = !viewMatrix.isIdentity();
		// z = current renderer depth (Renderable.preDraw); a no-op under ortho,
		// consumed by perspective (Camera3d).
		const z = this.renderer.currentDepth;

		// each line pair (2 verts) expands to 2 triangles (6 verts)
		const expandedCount = (vertexCount / 2) * 6;

		if (vertexData.isFull(expandedCount)) {
			this.flush();
		}

		// switch to TRIANGLES mode
		if (this.mode !== this.gl.TRIANGLES) {
			this.flush(this.mode);
			this.mode = this.gl.TRIANGLES;
		}

		const m = hasTransform ? viewMatrix.val : null;

		for (let i = 0; i < vertexCount; i += 2) {
			const from = verts[i];
			const to = verts[i + 1];

			// apply view matrix to base positions without mutating
			// inputs. Includes the z column for parity with the simple-
			// line path and Vector3d quad batcher — Camera3d's view
			// matrix needs depth-aware rotation. Note: the perpendicular
			// normal is still computed in pre-projection world space,
			// which appears non-perpendicular under perspective — known
			// limitation, separate from the Vector3d migration.
			let fromX, fromY, fromZ, toX, toY, toZ;
			if (hasTransform) {
				fromX = from.x * m[0] + from.y * m[4] + z * m[8] + m[12];
				fromY = from.x * m[1] + from.y * m[5] + z * m[9] + m[13];
				fromZ = from.x * m[2] + from.y * m[6] + z * m[10] + m[14];
				toX = to.x * m[0] + to.y * m[4] + z * m[8] + m[12];
				toY = to.x * m[1] + to.y * m[5] + z * m[9] + m[13];
				toZ = to.x * m[2] + to.y * m[6] + z * m[10] + m[14];
			} else {
				fromX = from.x;
				fromY = from.y;
				fromZ = z;
				toX = to.x;
				toY = to.y;
				toZ = z;
			}

			// compute perpendicular unit normal
			const dx = toX - fromX;
			const dy = toY - fromY;
			const len = Math.sqrt(dx * dx + dy * dy);

			if (len === 0) {
				continue;
			}

			const nx = -dy / len;
			const ny = dx / len;

			// two triangles forming a quad around the line segment
			// triangle 1: from+n, from-n, to-n
			vertexData.push(fromX, fromY, fromZ, nx, ny, colorUint32);
			vertexData.push(fromX, fromY, fromZ, -nx, -ny, colorUint32);
			vertexData.push(toX, toY, toZ, -nx, -ny, colorUint32);

			// triangle 2: from+n, to-n, to+n
			vertexData.push(fromX, fromY, fromZ, nx, ny, colorUint32);
			vertexData.push(toX, toY, toZ, -nx, -ny, colorUint32);
			vertexData.push(toX, toY, toZ, nx, ny, colorUint32);
		}
	}
}
