import { loadImage, loadText, makePassFBO, makeDoubleBuffer, makePass } from "./utils.js";

const extractEntries = (src, keys) => Object.fromEntries(Array.from(Object.entries(src)).filter(([key]) => keys.includes(key)));

const rippleTypes = {
	box: 0,
	circle: 1,
};

// These compute buffers are used to compute the properties of cells in the grid.
// They take turns being the source and destination of a "compute" shader.
// The half float data type is crucial! It lets us store almost any real number,
// whereas the default type limits us to integers between 0 and 255.

// These double buffers are smaller than the screen, because their pixels correspond
// with cells in the grid, and the cells' glyphs are much larger than a pixel.
const makeComputeDoubleBuffer = (regl, height, width) =>
	makeDoubleBuffer(regl, {
		width,
		height,
		wrapT: "clamp",
		type: "half float",
	});

const numVerticesPerQuad = 2 * 3;
const tlVert = [0, 0];
const trVert = [0, 1];
const blVert = [1, 0];
const brVert = [1, 1];
const quadVertices = [tlVert, trVert, brVert, tlVert, brVert, blVert];

export default ({ regl, config, lkg }) => {
	// The volumetric mode multiplies the number of columns
	// to reach the desired density, and then overlaps them
	const volumetric = config.volumetric;
	const density = volumetric && config.effect !== "none" ? config.density : 1;
	const [numRows, numColumns] = [config.numColumns, Math.floor(config.numColumns * density)];

	// The volumetric mode requires us to create a grid of quads,
	// rather than a single quad for our geometry
	const [numQuadRows, numQuadColumns] = volumetric ? [numRows, numColumns] : [1, 1];
	const numQuads = numQuadRows * numQuadColumns;
	const quadSize = [1 / numQuadColumns, 1 / numQuadRows];

	// Various effect-related values
	const rippleType = config.rippleTypeName in rippleTypes ? rippleTypes[config.rippleTypeName] : -1;
	const slantVec = [Math.cos(config.slant), Math.sin(config.slant)];
	const slantScale = 1 / (Math.abs(Math.sin(2 * config.slant)) * (Math.sqrt(2) - 1) + 1);
	const showDebugView = config.effect === "none";

	const commonUniforms = {
		...extractEntries(config, ["animationSpeed", "glyphHeightToWidth", "glyphSequenceLength", "glyphTextureGridSize"]),
		numColumns,
		numRows,
		showDebugView,
	};

	const introDoubleBuffer = makeComputeDoubleBuffer(regl, 1, numColumns);
	const rainPassIntro = loadText("shaders/glsl/rainPass.intro.frag.glsl");
	const introUniforms = {
		...commonUniforms,
		...extractEntries(config, ["fallSpeed", "skipIntro"]),
	};
	const intro = regl({
		frag: regl.prop("frag"),
		uniforms: {
			...introUniforms,
			previousIntroState: introDoubleBuffer.back,
		},

		framebuffer: introDoubleBuffer.front,
	});

	const raindropDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns);
	const rainPassRaindrop = loadText("shaders/glsl/rainPass.raindrop.frag.glsl");
	const raindropUniforms = {
		...commonUniforms,
		...extractEntries(config, ["brightnessDecay", "fallSpeed", "raindropLength", "loops", "skipIntro"]),
	};
	const raindrop = regl({
		frag: regl.prop("frag"),
		uniforms: {
			...raindropUniforms,
			introState: introDoubleBuffer.front,
			previousRaindropState: raindropDoubleBuffer.back,
		},

		framebuffer: raindropDoubleBuffer.front,
	});

	const symbolDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns);
	const rainPassSymbol = loadText("shaders/glsl/rainPass.symbol.frag.glsl");
	const symbolUniforms = {
		...commonUniforms,
		...extractEntries(config, ["cycleSpeed", "cycleFrameSkip", "loops"]),
	};
	const symbol = regl({
		frag: regl.prop("frag"),
		uniforms: {
			...symbolUniforms,
			raindropState: raindropDoubleBuffer.front,
			previousSymbolState: symbolDoubleBuffer.back,
		},

		framebuffer: symbolDoubleBuffer.front,
	});

	const effectDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns);
	const rainPassEffect = loadText("shaders/glsl/rainPass.effect.frag.glsl");
	const effectUniforms = {
		...commonUniforms,
		...extractEntries(config, ["hasThunder", "rippleScale", "rippleSpeed", "rippleThickness", "loops"]),
		rippleType,
	};
	const effect = regl({
		frag: regl.prop("frag"),
		uniforms: {
			...effectUniforms,
			raindropState: raindropDoubleBuffer.front,
			previousEffectState: effectDoubleBuffer.back,
		},

		framebuffer: effectDoubleBuffer.front,
	});

	const quadPositions = Array(numQuadRows)
		.fill()
		.map((_, y) =>
			Array(numQuadColumns)
				.fill()
				.map((_, x) => Array(numVerticesPerQuad).fill([x, y]))
		);

	// We render the code into an FBO using MSDFs: https://github.com/Chlumsky/msdfgen
	const glyphMSDF = loadImage(regl, config.glyphMSDFURL);
	const glintMSDF = loadImage(regl, config.glintMSDFURL);
	const baseTexture = loadImage(regl, config.baseTextureURL);
	const glintTexture = loadImage(regl, config.glintTextureURL);
	const rainPassVert = loadText("shaders/glsl/rainPass.vert.glsl");
	const rainPassFrag = loadText("shaders/glsl/rainPass.frag.glsl");
	const output = makePassFBO(regl, config.useHalfFloat);
	const renderUniforms = {
		...commonUniforms,
		...extractEntries(config, [
			// vertex
			"forwardSpeed",
			"glyphVerticalSpacing",
			// fragment
			"baseBrightness",
			"baseContrast",
			"glintBrightness",
			"glintContrast",
			"hasBaseTexture",
			"hasGlintTexture",
			"brightnessThreshold",
			"brightnessOverride",
			"isolateCursor",
			"isolateGlint",
			"glyphEdgeCrop",
			"isPolar",
		]),
		density,
		numQuadColumns,
		numQuadRows,
		quadSize,
		slantScale,
		slantVec,
		volumetric,
	};
	const render = regl({
		blend: {
			enable: true,
			func: {
				src: "one",
				dst: "one",
			},
		},
		vert: regl.prop("vert"),
		frag: regl.prop("frag"),

		uniforms: {
			...renderUniforms,

			raindropState: raindropDoubleBuffer.front,
			symbolState: symbolDoubleBuffer.front,
			effectState: effectDoubleBuffer.front,
			glyphMSDF: glyphMSDF.texture,
			glintMSDF: glintMSDF.texture,
			baseTexture: baseTexture.texture,
			glintTexture: glintTexture.texture,

			msdfPxRange: 4.0,
			glyphMSDFSize: () => [glyphMSDF.width(), glyphMSDF.height()],
			glintMSDFSize: () => [glintMSDF.width(), glintMSDF.height()],

			camera: regl.prop("camera"),
			transform: regl.prop("transform"),
			screenSize: regl.prop("screenSize"),
		},

		viewport: regl.prop("viewport"),

		attributes: {
			aPosition: quadPositions,
			aCorner: Array(numQuads).fill(quadVertices),
		},
		count: numQuads * numVerticesPerQuad,

		framebuffer: output,
	});

	// Camera and transform math for the volumetric mode
	const screenSize = [1, 1];
	const { mat4, vec3 } = glMatrix;
	const transform = mat4.create();
	if (volumetric && config.isometric) {
		mat4.rotateX(transform, transform, (Math.PI * 1) / 8);
		mat4.rotateY(transform, transform, (Math.PI * 1) / 4);
		mat4.translate(transform, transform, vec3.fromValues(0, 0, -1));
		mat4.scale(transform, transform, vec3.fromValues(1, 1, 2));
	} else if (lkg.enabled) {
		mat4.translate(transform, transform, vec3.fromValues(0, 0, -1.1));
		mat4.scale(transform, transform, vec3.fromValues(1, 1, 1));
		mat4.scale(transform, transform, vec3.fromValues(0.15, 0.15, 0.15));
	} else {
		mat4.translate(transform, transform, vec3.fromValues(0, 0, -1));
	}
	const camera = mat4.create();

	const vantagePoints = [];

	return makePass(
		{
			primary: output,
		},
		Promise.all([
			glyphMSDF.loaded,
			glintMSDF.loaded,
			baseTexture.loaded,
			glintTexture.loaded,
			rainPassIntro.loaded,
			rainPassRaindrop.loaded,
			rainPassSymbol.loaded,
			rainPassVert.loaded,
			rainPassFrag.loaded,
		]),
		(w, h) => {
			output.resize(w, h);
			const aspectRatio = w / h;

			const [numTileColumns, numTileRows] = [lkg.tileX, lkg.tileY];
			const numVantagePoints = numTileRows * numTileColumns;
			const tileWidth = Math.floor(w / numTileColumns);
			const tileHeight = Math.floor(h / numTileRows);
			vantagePoints.length = 0;
			for (let row = 0; row < numTileRows; row++) {
				for (let column = 0; column < numTileColumns; column++) {
					const index = column + row * numTileColumns;
					const camera = mat4.create();

					if (volumetric && config.isometric) {
						if (aspectRatio > 1) {
							mat4.ortho(camera, -1.5 * aspectRatio, 1.5 * aspectRatio, -1.5, 1.5, -1000, 1000);
						} else {
							mat4.ortho(camera, -1.5, 1.5, -1.5 / aspectRatio, 1.5 / aspectRatio, -1000, 1000);
						}
					} else if (lkg.enabled) {
						mat4.perspective(camera, (Math.PI / 180) * lkg.fov, lkg.quiltAspect, 0.0001, 1000);

						const distanceToTarget = -1; // TODO: Get from somewhere else
						let vantagePointAngle = (Math.PI / 180) * lkg.viewCone * (index / (numVantagePoints - 1) - 0.5);
						if (isNaN(vantagePointAngle)) {
							vantagePointAngle = 0;
						}
						const xOffset = distanceToTarget * Math.tan(vantagePointAngle);

						mat4.translate(camera, camera, vec3.fromValues(xOffset, 0, 0));

						camera[8] = -xOffset / (distanceToTarget * Math.tan((Math.PI / 180) * 0.5 * lkg.fov) * lkg.quiltAspect); // Is this right??
					} else {
						mat4.perspective(camera, (Math.PI / 180) * 90, aspectRatio, 0.0001, 1000);
					}

					const viewport = {
						x: column * tileWidth,
						y: row * tileHeight,
						width: tileWidth,
						height: tileHeight,
					};
					vantagePoints.push({ camera, viewport });
				}
			}
			[screenSize[0], screenSize[1]] = aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1];
		},
		(shouldRender) => {
			intro({ frag: rainPassIntro.text() });
			raindrop({ frag: rainPassRaindrop.text() });
			symbol({ frag: rainPassSymbol.text() });
			effect({ frag: rainPassEffect.text() });

			if (shouldRender) {
				regl.clear({
					depth: 1,
					color: [0, 0, 0, 1],
					framebuffer: output,
				});

				for (const vantagePoint of vantagePoints) {
					render({ ...vantagePoint, transform, screenSize, vert: rainPassVert.text(), frag: rainPassFrag.text() });
				}
			}
		}
	);
};
