import { Vector3d } from "../../../math/vector3d.ts";
import IndexBuffer from "../buffer/index.js";
import { buildMultiTextureFragment } from "./../shaders/multitexture.js";
import quadMultiVertex from "./../shaders/quad-multi.vert";
import { MaterialBatcher } from "./material_batcher.js";

/**
 * additional import for TypeScript
 * @import {TextureAtlas} from "./../../texture/atlas.js";
 */

// a pool of reusable vectors used by `addQuad` to transform the four
// quad corners. Vector3d (not Vector2d) so the per-sprite depth set on
// `z` flows through `Matrix3d.apply` — required for Camera3d's view
// matrix (Y/X-axis rotation) to actually rotate the vertex in 3D space.
// For 2D-only matrices the z column is identity, so `(x, y)` output is
// bit-identical to the Vector2d path. Exported so `LitQuadBatcher`
// reuses the same pool — JS is single-threaded and `addQuad` is
// synchronous, so concurrent access can't happen.
export const V_ARRAY = [
	new Vector3d(),
	new Vector3d(),
	new Vector3d(),
	new Vector3d(),
];

/**
 * 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 QuadBatcher extends MaterialBatcher {
	/**
	 * Initialize the compositor
	 * @ignore
	 */
	init(renderer) {
		/**
		 * the maximum number of texture units used for multi-texture batching
		 * @type {number}
		 * @ignore
		 */
		this.maxBatchTextures = Math.min(renderer.maxTextures, 16);

		super.init(renderer, {
			attributes: [
				{
					// vec3: (x, y, z). z carries `renderable.depth` for
					// perspective projection (Camera3d). Stride = 28 bytes.
					name: "aVertex",
					size: 3,
					type: renderer.gl.FLOAT,
					normalized: false,
					offset: 0 * Float32Array.BYTES_PER_ELEMENT,
				},
				{
					name: "aRegion",
					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,
				},
				{
					name: "aTextureId",
					size: 1,
					type: renderer.gl.FLOAT,
					normalized: false,
					offset: 6 * Float32Array.BYTES_PER_ELEMENT,
				},
			],
			shader: {
				vertex: quadMultiVertex,
				fragment: buildMultiTextureFragment(this.maxBatchTextures),
			},
		});

		this.bindColorSamplers();

		/**
		 * whether multi-texture batching is currently active
		 * (disabled when a custom ShaderEffect is applied)
		 * @type {boolean}
		 * @ignore
		 */
		this.useMultiTexture = true;

		this.createIndexBuffer();
	}

	/**
	 * (Re-)create the index buffer for quad batching (4 verts + 6 indices per quad).
	 * Called from `init` and `reset` (after context loss).
	 * @ignore
	 */
	createIndexBuffer() {
		const maxQuads = this.vertexData.maxVertex / 4;
		this.indexBuffer = new IndexBuffer(
			this.gl,
			maxQuads * 6,
			this.renderer.WebGLVersion > 1,
		);
		this.indexBuffer.fillQuadPattern(maxQuads);
	}

	/**
	 * Bind the color sampler uniforms (`uSampler0..uSamplerN-1`) to their
	 * respective texture units. Called from `init` and `reset`.
	 * @ignore
	 */
	bindColorSamplers() {
		for (let i = 0; i < this.maxBatchTextures; i++) {
			this.defaultShader.setUniform("uSampler" + i, i);
		}
	}

	/**
	 * Select the shader to use for compositing.
	 * Multi-texture batching is automatically enabled when the default
	 * shader is active, and disabled for custom ShaderEffect shaders.
	 * @see GLShader
	 * @see ShaderEffect
	 * @param {GLShader|ShaderEffect} shader - a reference to a GLShader or ShaderEffect instance
	 */
	useShader(shader) {
		super.useShader(shader);
		this.useMultiTexture = shader === this.defaultShader;
	}

	/**
	 * Reset compositor internal state
	 * @ignore
	 */
	reset() {
		super.reset();
		this.createIndexBuffer();
		this.bindColorSamplers();
		this.useMultiTexture = true;
	}

	/**
	 * Flush batched texture data to the GPU using indexed drawing.
	 * @param {number} [mode=gl.TRIANGLES] - the GL drawing mode
	 */
	flush(mode = this.mode) {
		const vertex = this.vertexData;
		const vertexCount = vertex.vertexCount;

		if (vertexCount > 0) {
			const gl = this.gl;
			const vertexSize = vertex.vertexSize;

			// ensure the index buffer is bound
			this.indexBuffer.bind();

			// Byte-view upload — keeps packed-color bytes intact across
			// drivers that canonicalize NaN-pattern Float32 values on
			// upload (see `VertexArrayBuffer.bufferU8`).
			const byteLength =
				vertexCount * vertexSize * Float32Array.BYTES_PER_ELEMENT;
			if (this.renderer.WebGLVersion > 1) {
				gl.bufferData(
					gl.ARRAY_BUFFER,
					vertex.toUint8(),
					gl.STREAM_DRAW,
					0,
					byteLength,
				);
			} else {
				gl.bufferData(
					gl.ARRAY_BUFFER,
					vertex.toUint8(0, byteLength),
					gl.STREAM_DRAW,
				);
			}

			// 4 vertices per quad -> vertexCount/4 quads -> *6 indices per quad
			const indexCount = (vertexCount / 4) * 6;
			gl.drawElements(mode, indexCount, this.indexBuffer.type, 0);

			vertex.clear();
		}
	}

	/**
	 * Draw a screen-aligned quad with the given raw WebGL texture through the given shader.
	 * Binds the texture to unit 0, pushes 4 vertices (Y-flipped UVs), flushes,
	 * then unbinds the texture.
	 * @param {WebGLTexture} source - the raw GL texture to blit
	 * @param {number} x - destination x
	 * @param {number} y - destination y
	 * @param {number} width - destination width
	 * @param {number} height - destination height
	 * @param {GLShader|ShaderEffect} shader - the shader effect to apply
	 */
	blitTexture(source, x, y, width, height, shader) {
		const gl = this.gl;

		this.useShader(shader);

		// bind the source texture to unit 0 — keep the batcher's internal
		// texture-unit bookkeeping in sync with the actual GL state, or
		// later `bindTexture2D` calls may early-out on a stale
		// `currentTextureUnit` and bind to the wrong unit.
		gl.activeTexture(gl.TEXTURE0);
		gl.bindTexture(gl.TEXTURE_2D, source);
		this.currentTextureUnit = 0;
		this.boundTextures[0] = source;
		shader.setUniform("uSampler", 0);

		// push a screen-aligned quad with Y-flipped UVs, transformed by
		// the current renderer transform. `WebGLRenderer.blitEffect` (the
		// only caller today) resets `currentTransform` to identity before
		// calling so FBO blits land in screen space; the matrix path is
		// kept for any future world-space caller that wants its preDraw
		// translate/scale honored.
		const m = this.viewMatrix;
		// blits are always at z = 0 (screen-space). Setting z explicitly
		// matters because V_ARRAY is Vector3d — any leftover z from a
		// prior addQuad would otherwise leak into the transform.
		const vec0 = V_ARRAY[0].set(x, y, 0);
		const vec1 = V_ARRAY[1].set(x + width, y, 0);
		const vec2 = V_ARRAY[2].set(x, y + height, 0);
		const vec3 = V_ARRAY[3].set(x + width, y + height, 0);
		if (m && !m.isIdentity()) {
			m.apply(vec0);
			m.apply(vec1);
			m.apply(vec2);
			m.apply(vec3);
		}

		// blits stay at z = 0 (screen-space, ortho); pre-PR behavior
		// preserved unchanged
		const tint = 0xffffffff;
		this.vertexData.push(vec0.x, vec0.y, 0, 0, 1, tint, 0);
		this.vertexData.push(vec1.x, vec1.y, 0, 1, 1, tint, 0);
		this.vertexData.push(vec2.x, vec2.y, 0, 0, 0, tint, 0);
		this.vertexData.push(vec3.x, vec3.y, 0, 1, 0, tint, 0);

		this.flush();

		// unbind the texture to prevent feedback loop on next frame, and
		// drop the unit-0 cache slot so the next bind re-issues activeTexture.
		gl.activeTexture(gl.TEXTURE0);
		gl.bindTexture(gl.TEXTURE_2D, null);
		this.currentTextureUnit = -1;
		delete this.boundTextures[0];

		// restore the default shader (also re-enables multi-texture batching)
		this.useShader(this.defaultShader);
	}

	/**
	 * Add a textured quad
	 * @param {TextureAtlas} texture - Source texture atlas
	 * @param {number} x - Destination x-coordinate
	 * @param {number} y - Destination y-coordinate
	 * @param {number} w - Destination width
	 * @param {number} h - Destination height
	 * @param {number} u0 - Texture UV (u0) value.
	 * @param {number} v0 - Texture UV (v0) value.
	 * @param {number} u1 - Texture UV (u1) value.
	 * @param {number} v1 - Texture UV (v1) value.
	 * @param {number} tint - tint color to be applied to the texture in UINT32 (argb) format
	 * @param {boolean} [reupload=false] - Force the texture to be reuploaded even if already bound
	 */
	addQuad(texture, x, y, w, h, u0, v0, u1, v1, tint, reupload = false) {
		const vertexData = this.vertexData;

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

		let unit;

		if (this.useMultiTexture) {
			// multi-texture path: embed the texture unit in the vertex data
			// and avoid flushing on texture changes
			unit = this.uploadTexture(texture, w, h, reupload, false);
			// shader only supports maxBatchTextures samplers — flush and
			// reset if the cache assigned a unit beyond the shader's range
			if (unit >= this.maxBatchTextures) {
				this.flush();
				this.renderer.cache.resetUnitAssignments();
				unit = this.uploadTexture(texture, w, h, reupload, false);
			}
		} else {
			// single-texture fallback (custom ShaderEffect active):
			// use regular upload which flushes on texture change, and set uSampler
			unit = this.uploadTexture(texture, w, h, reupload);
			if (unit !== this.currentSamplerUnit) {
				this.currentShader.setUniform("uSampler", unit);
				this.currentSamplerUnit = unit;
			}
		}

		// Transform vertices. Stamp per-sprite depth onto z BEFORE
		// `m.apply` so Camera3d's view matrix (3D R⁻¹ ∘ T(-pos)) fully
		// rotates the vertex. For 2D-only matrices the z column is
		// identity, so output (x, y) is bit-identical to the legacy
		// Vector2d path and z passes through unchanged.
		const m = this.viewMatrix;
		const z = this.renderer.currentDepth;
		const vec0 = V_ARRAY[0].set(x, y, z);
		const vec1 = V_ARRAY[1].set(x + w, y, z);
		const vec2 = V_ARRAY[2].set(x, y + h, z);
		const vec3 = V_ARRAY[3].set(x + w, y + h, z);

		if (!m.isIdentity()) {
			m.apply(vec0);
			m.apply(vec1);
			m.apply(vec2);
			m.apply(vec3);
		}

		// 4 vertices per quad; the index buffer provides the 6 indices.
		// textureId is the unit index for multi-texture, or 0 for
		// single-texture fallback.
		const textureId = this.useMultiTexture ? unit : 0;
		vertexData.push(vec0.x, vec0.y, vec0.z, u0, v0, tint, textureId);
		vertexData.push(vec1.x, vec1.y, vec1.z, u1, v0, tint, textureId);
		vertexData.push(vec2.x, vec2.y, vec2.z, u0, v1, tint, textureId);
		vertexData.push(vec3.x, vec3.y, vec3.z, u1, v1, tint, textureId);
	}
}
