"use strict";
(() => {
	const makeSphere = (meridians = 3, bandsPerHemisphere = 1) => {
		const assert = (cond, msg) => { if (!cond) throw (msg ? msg : "failed assertion"); };

		// The number of meridians dividing the sphere's surface.
		// Two meridians would make an XZ plane.
		assert(meridians >= 3);
		assert(Number.isInteger(meridians));

		// The number of latitude bands comprising each hemisphere.
		assert(bandsPerHemisphere >= 1);
		assert(Number.isInteger(bandsPerHemisphere));

		const parallels = bandsPerHemisphere * 2 + 1;

		// The seam needs two of each vertex so we can map the texture correctly.
		const vertices = new Float32Array((meridians + 1) * parallels * 5);
		const vertexStride = 5 * vertices.constructor.BYTES_PER_ELEMENT;
		const positionOffset = 0;
		const textureCoordOffset = 3 * vertices.constructor.BYTES_PER_ELEMENT;
		let verticesIdx = 0;
		/*
			In order to cut down on distortion at the poles, the parallels
			are not at equal intervals of latitude. Instead, the parallels
			describe the intersections of the (circum)sphere with a set of
			concentric, evenly spaced, vertically oriented cylinders. This
			makes the parallels denser towards the poles, in proportion to
			how badly that density is needed at any given latitude.
		*/
		for (let p = 0; p < parallels; p++) {
			const π = Math.PI;
			const sin = Math.sin;
			const lat = π * sin(π * (p - bandsPerHemisphere) / 2 / bandsPerHemisphere) / 2;
			const y = Math.sin(lat);
			for (let m = 0; m < meridians + 1; m++) {
				const long = m * 2 * Math.PI / meridians;
				const x = Math.cos(lat) * Math.sin(long);
				const z = Math.cos(lat) * Math.cos(long);
				const u = m / meridians;
				const v = lat / π + 0.5;

				vertices[verticesIdx++] = x;
				vertices[verticesIdx++] = y;
				vertices[verticesIdx++] = z;
				vertices[verticesIdx++] = u;
				vertices[verticesIdx++] = v;
			}
		}

		const indices = new Uint16Array(meridians * (parallels - 1) * 6);
		let indicesIdx = 0;
		for (let p = 0; p < parallels - 1; p++) {
			for (let m = 0; m < meridians; m++) {
				const lowerLeft = p * (meridians + 1) + m;
				const lowerRight = p * (meridians + 1) + (m + 1);
				const upperLeft = (p + 1) * (meridians + 1) + m;
				const upperRight = (p + 1) * (meridians + 1) + (m + 1);
				const currIndices = [
					// Lower triangle
					lowerLeft, lowerRight, upperRight,

					// Upper triangle
					lowerLeft, upperRight, upperLeft,
				];
				for (const index of currIndices) {
					indices[indicesIdx++] = index;
				}
			}
		}

		return {
			vertices, indices,
			vertexStride, positionOffset, textureCoordOffset,
		};
	}

	const initShaderProgram = (gl) => {
		const vShaderSrc = `
			attribute vec3 position;
			attribute vec2 textureCoord;

			uniform mat4 viewMatrix;
			uniform mat4 projectionMatrix;

			varying highp vec2 vTextureCoord;

			void main() {
				gl_Position = projectionMatrix * viewMatrix * vec4(position, 1);
				vTextureCoord = textureCoord;
			}
		`;

		const fShaderSrc = `
			varying highp vec2 vTextureCoord;
			uniform sampler2D sampler;

			void main() {
				gl_FragColor = texture2D(sampler, vTextureCoord);
			}
		`;

		const compileShader = (src, type) => {
			let shader = gl.createShader(type);
			gl.shaderSource(shader, src);
			gl.compileShader(shader);
			let success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
			if (!success) {
				throw gl.getShaderInfoLog(shader);
			}
			return shader;
		}

		let vShader = compileShader(vShaderSrc, gl.VERTEX_SHADER);
		let fShader = compileShader(fShaderSrc, gl.FRAGMENT_SHADER);

		let program = gl.createProgram();
		gl.attachShader(program, vShader);
		gl.attachShader(program, fShader);
		gl.linkProgram(program);
		let success = gl.getProgramParameter(program, gl.LINK_STATUS);
		if (!success) {
			throw gl.getProgramInfoLog(program);
		}
		return program;
	};

	const loadTexture = (gl, image, textureUnit = 0) => {
		const textureUnitName = `TEXTURE${textureUnit}`
		const texture = gl.createTexture();
		const samplerLocation = gl.getUniformLocation(program, "sampler");
		image.addEventListener("load", () => {
			gl.activeTexture(gl[textureUnitName]);
			gl.bindTexture(gl.TEXTURE_2D, texture);
			gl.uniform1i(samplerLocation, textureUnit);
			gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
		});
		return texture;
	};

	let viewer = document.getElementById("viewer");
	let gl = viewer.getContext("webgl2");
	let program = initShaderProgram(gl);
	let viewMatrixLocation = gl.getUniformLocation(program, "viewMatrix");
	let projectionMatrixLocation = gl.getUniformLocation(program, "projectionMatrix");

	// Set up mouse-panning state machine.
	let yaw = 0;
	let pitch = 0;
	const PanState = Object.freeze({
		idle: (event) => {
			if (event.type === "mousedown") {
				return PanState.panning;
			}
			return PanState.idle;
		},
		panning: (event) => {
			switch (event.type) {
				case "mouseup":
					return PanState.idle;
				case "mouseover":
					if ((event.buttons & 1) == 0) {
						return PanState.idle;
					}
					break;
				case "mousemove":
					yaw += event.movementX * 0.005;
					pitch += event.movementY * 0.005;
					if (pitch < -Math.PI / 2) {
						pitch = -Math.PI / 2;
					} else if (pitch > Math.PI / 2) {
						pitch = Math.PI / 2;
					}
					break
			}
			return PanState.panning;
		},
	});
	const runPanState = (mouseEvent) => {
		panState = panState(mouseEvent);
	}
	var panState = PanState.idle;
	viewer.addEventListener("mousedown", runPanState);
	viewer.addEventListener("mouseup", runPanState);
	viewer.addEventListener("mouseover", runPanState);
	viewer.addEventListener("mousemove", runPanState);

	const minZoom = 1;
	let zoom = 3;
	viewer.addEventListener("wheel", (evt) => {
		zoom = Math.max(zoom * (1 - evt.deltaY * 0.005), minZoom);
		evt.preventDefault();
	});

	gl.useProgram(program);
	const sphere = makeSphere(24, 24);

	let vertexBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
	gl.bufferData(gl.ARRAY_BUFFER, sphere.vertices, gl.STATIC_DRAW);
	const positionLocation = gl.getAttribLocation(program, "position");
	const textureCoordLocation = gl.getAttribLocation(program, "textureCoord");

	let indexBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
	gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, sphere.indices, gl.STATIC_DRAW);

	gl.enable(gl.CULL_FACE);

	// Load texture
	let image = document.getElementById("image");
	loadTexture(gl, image);
	gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);

	const render = () => {
		gl.useProgram(program);

		// Create the transformation matrix.
		const viewMatrix = mat4.create();
		mat4.rotateX(viewMatrix, viewMatrix, pitch);
		mat4.rotateY(viewMatrix, viewMatrix, yaw);
		gl.uniformMatrix4fv(viewMatrixLocation, false, viewMatrix);

		const aspect = gl.drawingBufferWidth / gl.drawingBufferHeight;
		const projectionMatrix = mat4.ortho(mat4.create(), -aspect / zoom, aspect / zoom, -1 / zoom, 1 / zoom, -1, 1);
		gl.uniformMatrix4fv(projectionMatrixLocation, false, projectionMatrix);

		gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
		gl.enableVertexAttribArray(positionLocation);
		gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, sphere.vertexStride, sphere.positionOffset);
		gl.enableVertexAttribArray(textureCoordLocation);
		gl.vertexAttribPointer(textureCoordLocation, 2, gl.FLOAT, false, sphere.vertexStride, sphere.textureCoordOffset);

		gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
		gl.drawElements(gl.TRIANGLES, sphere.indices.length, gl.UNSIGNED_SHORT, 0);
		requestAnimationFrame(render);
	};

	requestAnimationFrame(render);
})();