class Portal {
	constructor(canvas, resourceDir) {

		// Vertex shader.
		const vglsl = `#version 300 es

		layout (location = 0) in vec3 Position;

		uniform mat4 modelViewMatrix;
		uniform mat4 projectionMatrix;
		
		uniform vec2 canvasResolution;

		vec4 projection_from_position(vec4 position) {
			vec4 projection = position * 0.5;
			projection.xy = vec2(projection.x + projection.w, projection.y + projection.w);
			projection.zw = position.zw;
			return projection;
		}

		out vec4 texProj0;

		void main() {
			gl_Position = projectionMatrix * modelViewMatrix * vec4(Position, 1.0);

			texProj0 = projection_from_position(gl_Position);
			texProj0 = vec4(texProj0.xy * canvasResolution / max(canvasResolution.x, canvasResolution.y), texProj0.zw);
		}`

		// Fragment shader.
		const fglsl = `#version 300 es

		precision highp float;

		uniform sampler2D sky;
		uniform sampler2D particles;
		uniform float dt;

		in vec4 texProj0;

		const int LAYERS = 15;
		const vec3 COLORS[] = vec3[](
			vec3(0.022087, 0.098399, 0.110818),
			vec3(0.011892, 0.095924, 0.089485),
			vec3(0.027636, 0.101689, 0.100326),
			vec3(0.046564, 0.109883, 0.114838),
			vec3(0.064901, 0.117696, 0.097189),
			vec3(0.063761, 0.086895, 0.123646),
			vec3(0.084817, 0.111994, 0.166380),
			vec3(0.097489, 0.154120, 0.091064),
			vec3(0.106152, 0.131144, 0.195191),
			vec3(0.097721, 0.110188, 0.187229),
			vec3(0.133516, 0.138278, 0.148582),
			vec3(0.070006, 0.243332, 0.235792),
			vec3(0.196766, 0.142899, 0.214696),
			vec3(0.047281, 0.315338, 0.321970),
			vec3(0.204675, 0.390010, 0.302066),
			vec3(0.080955, 0.314821, 0.661491)
		);

		const mat4 SCALE_TRANSLATE = mat4(
			0.5, 0.0, 0.0, 0.25,
			0.0, 0.5, 0.0, 0.25,
			0.0, 0.0, 1.0, 0.0,
			0.0, 0.0, 0.0, 1.0
		);

		mat2 mat2_rotate_z(float radians) {
			return mat2(
				cos(radians), -sin(radians),
				sin(radians), cos(radians)
			);
		}

		mat4 portal_layer(float layer) {
			mat4 translate = mat4(
				1.0, 0.0, 0.0, 17.0 / layer,
				0.0, 1.0, 0.0, (2.0 + layer / 1.5) * (dt * 1.5),
				0.0, 0.0, 1.0, 0.0,
				0.0, 0.0, 0.0, 1.0
			);

			mat2 rotate = mat2_rotate_z(radians((layer * layer * 4321.0 + layer * 9.0) * 2.0));

			mat2 scale = mat2((4.5 - layer / 4.0) * 2.0);

			return mat4(scale * rotate) * translate * SCALE_TRANSLATE;
		}

		out vec4 fragColor;

		void main() {
			vec3 color = textureProj(sky, texProj0).rgb * COLORS[0];
			
			for (int i = 0; i < LAYERS; i++) {
				color += textureProj(particles, texProj0 * portal_layer(float(i + 1))).rgb * COLORS[i];
			}
			
			fragColor = vec4(color, 1.0);
		}`
		const images = [
			`${resourceDir !== '/' ? resourceDir : ''}/sky.png`,
			`${resourceDir !== '/' ? resourceDir : ''}/portal.png`,
		];

		// Version & notice.
		this.version = '1.1';
		this.notice();

		this.canvas = canvas;
		this.gl = this.canvas.getContext('webgl2');

		// If WebGL isn't part of available features, fail.
		if (!this.gl) {
			alert("Unable to initialize WebGL 2. Your browser or machine may not support it.");
			return;
		}

		// Size the canvas to window initially.
		this.canvas.width = window.innerWidth;
		this.canvas.height = window.innerHeight;

		// Get current tick and slightly randomize it.
		this.time = Date.now() - this.randomRange(60 * 1000, 180 * 1000);

		// Build shaders.
		this.prog = this.build(vglsl, fglsl);

		if (!this.prog) {
			alert("Failed to compile WebGL 2 shaders.\nOpen the developer console for debug output.");
			return;
		}

		// Bind class context to the resize handler, pass it to the event bus.
		window.addEventListener('resize', this.resize.bind(this));

		// Create image loading promises.
		this.promises = images.map(image => this.loadImage(image));

		// Initialize the scene.
		this.initialize();

		// Once all promises have been fulfilled, build the scene.
		Promise.all(this.promises).then(resources => {

			// Load resources.
			resources.forEach((image, index) => this.loadResource(image, index));

			// Bind program
			this.gl.useProgram(this.prog);

			// Begin render.
			this.render();
		});
	}

	notice() {
		console.log(
			`%c \u2587%c\u2587%c\u2587  %c\u2587%c\u2587%c\u2587 %c // ECMAPortal v${this.version} by Endermanch & WiPet\n\n\thttps://enderman.ch\n\thttps://go.enderman.ch/wipet`,
			'color: #E58EFF',
			'color: #D52DFF',
			'color: #E58EFF',
			'color: #E58EFF',
			'color: #D52DFF',
			'color: #E58EFF',
			'color: #008000',
		);

		console.log(
			'If any errors occur below, please send a screenshot of them my way!\n\t%ccontact@enderman.ch',
			'color: #87CEFA'
		);
	}

	randomRange(min, max) {
		min = Math.ceil(min);
		max = Math.floor(max);

		return Math.floor(Math.random() * (max - min + 1)) + min;
	}

	build(vshSource, fshSource) {
		let vsh = this.gl.createShader(this.gl.VERTEX_SHADER);

		this.gl.shaderSource(vsh, vshSource);
		this.gl.compileShader(vsh);

		if (!this.gl.getShaderParameter(vsh, this.gl.COMPILE_STATUS)) {
			console.error(this.gl.getShaderInfoLog(vsh));
			return false;
		}

		let fsh = this.gl.createShader(this.gl.FRAGMENT_SHADER);

		this.gl.shaderSource(fsh, fshSource);
		this.gl.compileShader(fsh);

		if (!this.gl.getShaderParameter(fsh, this.gl.COMPILE_STATUS)) {
			console.error(this.gl.getShaderInfoLog(fsh));
			return false;
		}

		let program = this.gl.createProgram();

		this.gl.attachShader(program, vsh);
		this.gl.attachShader(program, fsh);

		this.gl.linkProgram(program);

		if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
			console.error(this.gl.getProgramInfoLog(program));
			return false;
		}

		return program;
	}

	loadImage(url) {
		return new Promise((resolve, reject) => {
			let img = new Image();

			img.onload = () => resolve(img);
			img.onerror = reject;
			img.src = url;
		});
	}

	loadResource(image, index) {
		const samplerParameters = [
			this.gl.CLAMP_TO_EDGE,
			this.gl.MIRRORED_REPEAT,
		];

		let sampler = this.gl.createSampler();

		this.gl.samplerParameteri(sampler, this.gl.TEXTURE_WRAP_S, samplerParameters[index]);
		this.gl.samplerParameteri(sampler, this.gl.TEXTURE_WRAP_T, samplerParameters[index]);
		this.gl.samplerParameteri(sampler, this.gl.TEXTURE_WRAP_R, samplerParameters[index]);

		this.gl.samplerParameteri(sampler, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST_MIPMAP_LINEAR);
		this.gl.samplerParameteri(sampler, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);

		this.gl.bindSampler(index, sampler);

		let texture = this.gl.createTexture();

		this.gl.activeTexture(this.gl.TEXTURE0 + index);
		this.gl.bindTexture(this.gl.TEXTURE_2D, texture);

		this.gl.texImage2D(
			this.gl.TEXTURE_2D,
			0,
			this.gl.RGBA,
			this.gl.RGBA,
			this.gl.UNSIGNED_BYTE,
			image
		);
		this.gl.generateMipmap(this.gl.TEXTURE_2D);
	}

	initialize() {
		this.uniforms = {
			modelViewMatrix: this.gl.getUniformLocation(this.prog, "modelViewMatrix"),
			projectionMatrix: this.gl.getUniformLocation(this.prog, "projectionMatrix"),
			dt: this.gl.getUniformLocation(this.prog, "dt"),
			resolution: this.gl.getUniformLocation(this.prog, "canvasResolution"),
			sky: this.gl.getUniformLocation(this.prog, "sky"),
			particles: this.gl.getUniformLocation(this.prog, "particles"),
		}

		let vertexPosBuffer = this.gl.createBuffer();
		let positions = new Float32Array([
			-1.0, -1.0,
			 1.0, -1.0,
			 1.0,  1.0,
			 1.0,  1.0,
			-1.0,  1.0,
			-1.0, -1.0
		]);

		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vertexPosBuffer);
		this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW);
		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);

		let vertexTexBuffer = this.gl.createBuffer();
		let texcoords = new Float32Array([
			0.0, 1.0,
			1.0, 1.0,
			1.0, 0.0,
			1.0, 0.0,
			0.0, 0.0,
			0.0, 1.0
		]);

		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vertexTexBuffer);
		this.gl.bufferData(this.gl.ARRAY_BUFFER, texcoords, this.gl.STATIC_DRAW);
		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);

		let vertexPosLocation = 0;

		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vertexPosBuffer);
		this.gl.vertexAttribPointer(vertexPosLocation, 2, this.gl.FLOAT, false, 0, 0);
		this.gl.enableVertexAttribArray(vertexPosLocation);
		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);

		let vertexTexLocation = 4;

		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vertexTexBuffer);
		this.gl.vertexAttribPointer(vertexTexLocation, 2, this.gl.FLOAT, false, 0, 0);
		this.gl.enableVertexAttribArray(vertexTexLocation);
		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);

	}

	scene() {
		let identityMatrix = new Float32Array([
			1.0, 0.0, 0.0, 0.0,
			0.0, 1.0, 0.0, 0.0,
			0.0, 0.0, 1.0, 0.0,
			0.0, 0.0, 0.0, 1.0
		]);

		let dt = (Date.now() - this.time) / 1000 / 1000;

		this.gl.uniformMatrix4fv(this.uniforms.modelViewMatrix, false, identityMatrix);
		this.gl.uniformMatrix4fv(this.uniforms.projectionMatrix, false, identityMatrix);

		this.gl.uniform2f(this.uniforms.resolution, this.canvas.clientWidth, this.canvas.clientHeight);

		this.gl.uniform1f(this.uniforms.dt, dt);
		this.gl.uniform1i(this.uniforms.sky, 0);
		this.gl.uniform1i(this.uniforms.particles, 1);

		this.gl.drawArraysInstanced(this.gl.TRIANGLES, 0, 6, 1);
	}

	render() {
		// Make sure the viewport matches the size of the canvas' drawingBuffer.
		this.gl.viewport(0, 0, this.gl.drawingBufferWidth, this.gl.drawingBufferHeight);

		// Run the scene.
		this.scene();

		// Request the next animation frame.
		requestAnimationFrame(this.render.bind(this));
	}

	resize() {
		let width = window.innerWidth;
		let height = window.innerHeight;

		if (
			this.canvas.width !== width ||
			this.canvas.height !== height
		) {
			this.canvas.width = width;
			this.canvas.height = height;
		}
	}

	destruct() {
		// Doesn't need to be called usually, the garbage collector should do its job.
		// The method isn't implemented, but the pseudocode is as follows:

		this.gl.deleteBuffer(vertexPosBuffer);
		this.gl.deleteBuffer(vertexTexBuffer);

		this.gl.deleteSampler(sampler);
		this.gl.deleteTexture(texture);

		this.gl.deleteProgram(this.prog);
	}
}

const canvas = document.querySelector('#ecmaportal');
const portal = new Portal(canvas, '/images/portal');
