// @ts-ignore
import mat4 from 'gl-mat4';
// @ts-ignore
import {convert} from './objparser';
import $ from 'jquery';

import {initBuffers, deleteBuffers} from './buffers';
import {initShaderProgram} from './shaders';
import {changeFragmentShader} from './changeshader';
import {loadTexture} from './texture';

let squareRotation = 0.0;

main();

/**
 * The program purpose is encapsulated in a main function
 */
async function main() {
	const canvas: any = document.querySelector('#glCanvas')!;
	const gl = canvas.getContext('webgl');

	if (gl == null) {
		canvas.parentNode.removeChild(canvas);
		document.getElementById('root')!.insertAdjacentHTML('beforeend',
			`<p>Unable to initialize WebGL. Your browser or machine may not
			support it.</p>`);
	}

	/* eslint-disable */

	const vsSource = `
		attribute vec4 aVertexPosition;
		attribute vec4 aVertexNormal;
		attribute vec2 aTextureCoord;

		uniform mat4 uProjectionMatrix;
		uniform mat4 uviewMatrix;
		uniform mat4 umodelMatrix;
		uniform mat4 unormalModelMatrix;

		varying highp vec4 vNormal;
		varying highp vec3 vPosition;
		varying highp vec2 vTextureCoord;

		void main()
		{
			gl_Position = uProjectionMatrix *
				uviewMatrix *
				unormalModelMatrix *
				umodelMatrix *
				aVertexPosition;
			vPosition = vec3(aVertexPosition);
			vNormal = unormalModelMatrix * aVertexNormal;
			vTextureCoord = aTextureCoord;
		}
	`;

	const fsSource = `
	precision highp float;

    varying highp vec2 vTextureCoord;
	varying highp vec4 vNormal;
	varying highp vec3 vPosition;

	uniform sampler2D uSampler;

	vec3 extremize(vec3 v, float n) {
		if (v.x > n / 2.)
			v.x = n;
		else
			v.x = 0.;
		if (v.y > n / 2.)
			v.y = n;
		else
			v.y = 0.;
		if (v.z > n / 2.)
			v.z = n;
		else
			v.z = 0.;
		return v;
	}

	void main() {
		vec3 n = normalize(vec3(-50000., 100000., 50000.) - vPosition);
		float diffuse = max(dot(normalize(vNormal.xyz), n), 0.);
		float specular = pow(
			max(dot(
				reflect(n, normalize(vNormal.xyz)),
				normalize(vec3(0., 0., -50.) - vPosition)),
			0.), 10.);
		vec3 tmp = extremize(mod(vPosition.xyz + vec3(1000.), vec3(2.)), 2.);
		float texture = (tmp.x + tmp.y + tmp.z) / 6.;
		gl_FragColor = vec4((texture * diffuse * 0.8) + (texture * vec3(0.2)) + (specular * vec3(1.)), 1.0);
	}`;

	const fsSource2 = `
	precision highp float;

    varying highp vec2 vTextureCoord;
	varying highp vec4 vNormal;
	varying highp vec3 vPosition;

	uniform sampler2D uSampler;

	vec3 extremize(vec3 v, float n) {
		if (v.x > n / 2.)
			v.x = n;
		else
			v.x = 0.;
		if (v.y > n / 2.)
			v.y = n;
		else
			v.y = 0.;
		if (v.z > n / 2.)
			v.z = n;
		else
			v.z = 0.;
		return v;
	}

	void main() {
		vec3 n = normalize(vec3(-50000., 100000., 50000.) - vPosition);
		float diffuse = max(dot(normalize(vNormal.xyz), n), 0.);
		float specular = pow(
			max(dot(
				reflect(n, normalize(vNormal.xyz)),
				normalize(vec3(0., 0., -50.) - vPosition)),
			0.), 10.);
		vec3 texture = extremize(mod(vPosition.xyz + vec3(1000.), vec3(2.)), 2.) / vec3(2);
		gl_FragColor = vec4((texture * diffuse * 0.8) + (texture * vec3(0.2)) + (specular * vec3(1.)), 1.0);
	}`;

	const fsSource3 = `
	precision highp float;

    varying highp vec2 vTextureCoord;
	varying highp vec4 vNormal;
	varying highp vec3 vPosition;

	uniform sampler2D uSampler;

	void main() {
		vec3 n = normalize(vec3(-50000., 100000., 50000.) - vPosition);
		float diffuse = max(dot(normalize(vNormal.xyz), n), 0.);
		float specular = pow(
			max(dot(
				reflect(n, normalize(vNormal.xyz)),
				normalize(vec3(0., 0., -50.) - vPosition)),
			0.), 10.);
		gl_FragColor = vec4((diffuse * 0.8) + (vec3(0.2)) + (specular * vec3(1.)), 1.0);
	}`;

	const fsSource4 = `
	precision highp float;
	
    varying highp vec2 vTextureCoord;

	varying highp vec4 vNormal;
	varying highp vec3 vPosition;
    uniform sampler2D uSampler;

	void main() {
		highp vec4 texelColor = texture2D(uSampler, vTextureCoord);
		vec3 n = normalize(vec3(-50000., 100000., 50000.) - vPosition);
		float diffuse = max(dot(normalize(vNormal.xyz), n), 0.);
		float specular = pow(
			max(dot(
				reflect(n, normalize(vNormal.xyz)),
				normalize(vec3(0., 0., -50.) - vPosition)),
			0.), 10.);
		gl_FragColor = vec4((texelColor.xyz * diffuse * 0.8) + (texelColor.xyz * vec3(0.2)) + (specular * vec3(1.)), 1.0);
	}`;
	
	const fsSource5 = `
	precision highp float;

    varying highp vec2 vTextureCoord;
	varying highp vec4 vNormal;
	varying highp vec3 vPosition;

	uniform sampler2D uSampler;

	void make_kernel(inout vec4 n[9], sampler2D tex, vec2 coord)
	{
		float w = 1.0 / 640.;
		float h = 1.0 / 640.;

		n[0] = texture2D(tex, coord + vec2( -w, -h));
		n[1] = texture2D(tex, coord + vec2(0.0, -h));
		n[2] = texture2D(tex, coord + vec2(  w, -h));
		n[3] = texture2D(tex, coord + vec2( -w, 0.0));
		n[4] = texture2D(tex, coord);
		n[5] = texture2D(tex, coord + vec2(  w, 0.0));
		n[6] = texture2D(tex, coord + vec2( -w, h));
		n[7] = texture2D(tex, coord + vec2(0.0, h));
		n[8] = texture2D(tex, coord + vec2(  w, h));
	}

	void main(void) 
	{
		vec4 n[9];
		make_kernel( n, uSampler, vTextureCoord);

		vec4 sobel_edge_h = n[2] + (2.0*n[5]) + n[8] - (n[0] + (2.0*n[3]) + n[6]);
		vec4 sobel_edge_v = n[0] + (2.0*n[1]) + n[2] - (n[6] + (2.0*n[7]) + n[8]);
		vec4 sobel = sqrt((sobel_edge_h * sobel_edge_h) + (sobel_edge_v * sobel_edge_v));

		if ((1. - sobel.r) + (1. - sobel.g) + (1. - sobel.b) < 2.5)
			gl_FragColor = vec4(1.0 - sobel.rgb, 1.0 );
		else {
			vec3 n = normalize(vec3(-50000., 100000., 50000.) - vPosition);
			float diffuse = max(dot(normalize(vNormal.xyz), n), 0.);
			float specular = pow(
				max(dot(
					reflect(n, normalize(vNormal.xyz)),
					normalize(vec3(0., 0., -50.) - vPosition)),
				0.), 10.);
			gl_FragColor = vec4((diffuse * 0.8) + (vec3(0.2)) + (specular * vec3(1.)), 1.0);
		}
	}`;

	/* eslint-enable */

	/**
	 * Fetch an obj file
	 * @param {string} url the url to fetch the object from
	 * @return {string} the raw data of the obj file
	 */
	async function getObj(url: string) {
		const response = await fetch(url);
		const data = await response.text();
		return data;
	}

	const data = await getObj('/static/objs/racer.obj');
	const distance: any = $('#distance').val();
	const distanceFine: any = $('#distancefine').val();
	const params: any = {
		distance: parseFloat(distance) + parseFloat(distanceFine),
		circleSize: $('#circlesize').val(),
		fov: $('#fov').val(),
		avg: {
			x: 0,
			y: 0,
			z: 0,
		},
		rot: {
			x: $('#rotx').val(),
			y: $('#roty').val(),
			z: $('#rotz').val(),
		},
		rotSpeed: $('#rotspeed').val(),
	};

	const [
		positions,
		normals,
		uvs,
		indices,
	] = convert(data);
	let x = 0;
	let y = 0;
	let z = 0;
	for (let i = 0; i < positions.length; i++) {
		if (i % 3 == 0) {
			x += positions[i];
		} else if (i % 3 == 1) {
			y += positions[i];
		} else {
			z += positions[i];
		}
	}
	params.avg.x = x / (positions.length / 3);
	params.avg.y = y / (positions.length / 3);
	params.avg.z = z / (positions.length / 3);
	let length = indices.length;
	let [shaderProgram, fragmentShader]: any = initShaderProgram(gl,
		vsSource,
		fsSource4);

	let programInfo: any = {
		program: shaderProgram,
		attribLocations: {
			vertexPosition: gl.getAttribLocation(shaderProgram,
				'aVertexPosition'),
			vertexNormal: gl.getAttribLocation(shaderProgram,
				'aVertexNormal'),
			textureCoord: gl.getAttribLocation(shaderProgram,
				'aTextureCoord'),
		},
		uniformLocations: {
			projectionMatrix: gl.getUniformLocation(
				shaderProgram, 'uProjectionMatrix'),
			viewMatrix: gl.getUniformLocation(
				shaderProgram, 'uviewMatrix'),
			modelMatrix: gl.getUniformLocation(
				shaderProgram, 'umodelMatrix'),
			normalModelMatrix: gl.getUniformLocation(
				shaderProgram, 'unormalModelMatrix'),
			uSampler: gl.getUniformLocation(
				shaderProgram, 'uSampler'),
		},
	};

	let texture = loadTexture(gl, '/static/textures/racer.png');
	let buffers = initBuffers(gl, positions, indices, normals, uvs);
	let then = 0;
	let changed = false;

	/**
	 * Draws the scene repeatedly
	 * @param {number} now the current time
	 */
	function render(now: any) {
		now *= 0.001;
		const deltaTime = now - then;
		if (now >= 1 && changed == false) {
			changed = true;
		}
		then = now;
		drawScene(gl,
			programInfo,
			buffers,
			deltaTime,
			length,
			params,
			texture);
		requestAnimationFrame(render);
	}

	/**
	 * Pushes a new obj file to the gl buffer
	 * @param {string} data the obj file to push
	 */
	function updateObj(data: string) {
		const [
			positions,
			normals,
			uvs,
			indices,
		] = convert(data);
		length = indices.length;
		let x = 0;
		let y = 0;
		let z = 0;
		for (let i = 0; i < positions.length; i++) {
			if (i % 3 == 0) {
				x += positions[i];
			} else if (i % 3 == 1) {
				y += positions[i];
			} else {
				z += positions[i];
			}
		}
		params.avg.x = x / (positions.length / 3);
		params.avg.y = y / (positions.length / 3);
		params.avg.z = z / (positions.length / 3);
		deleteBuffers(gl, buffers);
		buffers = initBuffers(gl, positions, indices, normals, uvs);
	}

	$(function() {
		$('#distance').on('input', function() {
			const distance: any = $('#distance').val();
			const distanceFine: any = $('#distancefine').val();
			params.distance = parseFloat(distance) + parseFloat(distanceFine);
		});
		$('#distancefine').on('input', function() {
			const distance: any = $('#distance').val();
			const distanceFine: any = $('#distancefine').val();
			params.distance = parseFloat(distance) + parseFloat(distanceFine);
		});
		$('#circlesize').on('input', function() {
			const circleSize: any = $('#circlesize').val();
			params.circleSize = parseFloat(circleSize);
		});
		$('#rotx').on('input', function() {
			const rotx: any = $('#rotx').val();
			params.rot.x = parseFloat(rotx);
		});
		$('#roty').on('input', function() {
			const roty: any = $('#roty').val();
			params.rot.y = parseFloat(roty);
		});
		$('#rotz').on('input', function() {
			const rotz: any = $('#rotz').val();
			params.rot.z = parseFloat(rotz);
		});
		$('#rotspeed').on('input', function() {
			const rotSpeed: any = $('#rotspeed').val();
			params.rotSpeed = parseFloat(rotSpeed);
		});
		$('#fov').on('input', function() {
			const fov: any = $('#fov').val();
			params.fov = parseFloat(fov);
		});
		$('#s_blackandwhite').on('click', function() {
			[programInfo, fragmentShader] = changeFragmentShader(gl,
				shaderProgram, fragmentShader, fsSource, vsSource);
		});
		$('#s_color').on('click', function() {
			[programInfo, fragmentShader] = changeFragmentShader(gl,
				shaderProgram, fragmentShader, fsSource2, vsSource);
		});
		$('#s_grey').on('click', function() {
			[programInfo, fragmentShader] = changeFragmentShader(gl,
				shaderProgram, fragmentShader, fsSource3, vsSource);
		});
		$('#s_texture').on('click', function() {
			[programInfo, fragmentShader] = changeFragmentShader(gl,
				shaderProgram, fragmentShader, fsSource4, vsSource);
		});
		$('#s_sobel').on('click', function() {
			[programInfo, fragmentShader] = changeFragmentShader(gl,
				shaderProgram, fragmentShader, fsSource5, vsSource);
		});
		$('#o_sphere').on('click', async function() {
			const data = await getObj('/static/objs/sphere.obj');
			updateObj(data);
		});
		$('#o_teapot').on('click', async function() {
			const data = await getObj('/static/objs/teapot.obj');
			updateObj(data);
		});
		$('#o_fox').on('click', async function() {
			const data = await getObj('/static/objs/fox.obj');
			updateObj(data);
		});
		$('#o_mecha').on('click', async function() {
			const data = await getObj('/static/objs/mecha.obj');
			updateObj(data);
		});
		$('#o_racer').on('click', async function() {
			const data = await getObj('/static/objs/racer.obj');
			updateObj(data);
		});
		$('#t_wall').on('click', async function() {
			texture = loadTexture(gl, '/static/textures/wall.png');
		});
		$('#t_ice').on('click', async function() {
			texture = loadTexture(gl, '/static/textures/ice.png');
		});
		$('#t_noise').on('click', async function() {
			texture = loadTexture(gl, '/static/textures/noise.png');
		});
		$('#t_fox').on('click', async function() {
			texture = loadTexture(gl, '/static/textures/fox.png');
		});
		$('#t_racer').on('click', async function() {
			texture = loadTexture(gl, '/static/textures/racer.png');
		});
		$('#t_racer_wireframe').on('click', async function() {
			texture = loadTexture(gl, '/static/textures/racer_wireframe.png');
		});
	});
	requestAnimationFrame(render);
}

/**
 * Draw a webgl scene
 * @param {any} gl the WebGL context
 * @param {any} programInfo WebGL program information
 * @param {any} buffers the buffers to draw
 * @param {number} deltaTime the difference in time since last call
 * @param {number} length the index buffer length
 * @param {number} params various parameterss
 * @param {any} texture the texture to load
 */
function drawScene(gl: any,
	programInfo: any,
	buffers: any,
	deltaTime: number,
	length: number,
	params: any,
	texture: any) {
	gl.clearColor(0.0, 0.0, 0.0, 1.0);
	gl.clearDepth(1.0);
	gl.enable(gl.DEPTH_TEST);
	gl.depthFunc(gl.LEQUAL);
	// Clear the canvas before we start drawing on it.
	gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

	// Create a perspective matrix, a special matrix that is
	// used to simulate the distortion of perspective in a camera.
	// Our field of view is 45 degrees, with a width/height
	// ratio that matches the display size of the canvas
	// and we only want to see objects between 0.1 units
	// and 100 units away from the camera.
	const fieldOfView = params.fov * Math.PI / 180;
	const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
	const zNear = 0.1;
	const zFar = 1000.0;
	const projectionMatrix = mat4.create();

	// note: glmatrix.js always has the first argument
	// as the destination to receive the result.
	mat4.perspective(
		projectionMatrix,
		fieldOfView,
		aspect,
		zNear,
		zFar);


	const normalModelMatrix = mat4.create();

	mat4.rotateX(normalModelMatrix,
		normalModelMatrix,
		params.rot.x);
	mat4.rotateY(normalModelMatrix,
		normalModelMatrix,
		params.rot.y);
	mat4.rotateZ(normalModelMatrix,
		normalModelMatrix,
		params.rot.z);

	const modelMatrix = mat4.create();
	mat4.translate(modelMatrix,
		modelMatrix,
		[
			-params.avg.x,
			-params.avg.y,
			-params.avg.z,
		]);
	mat4.rotateY(modelMatrix,
		modelMatrix,
		squareRotation);

	// Set the drawing position to the "identity" point, which is
	// the center of the scene.
	const viewMatrix = mat4.create();

	mat4.translate(
		viewMatrix,
		viewMatrix, [
			Math.cos(squareRotation) * params.circleSize,
			Math.sin(squareRotation) * params.circleSize,
			0,
		]);

	mat4.translate(
		viewMatrix,
		viewMatrix,
		[0.0, 0.0, -params.distance]);

	// Tell WebGL how to pull out the positions from the position
	// buffer into the vertexPosition attribute.
	{
		const numComponents = 3;
		const type = gl.FLOAT;
		const normalize = false;
		const stride = 0;
		const offset = 0;
		gl.bindBuffer(gl.ARRAY_BUFFER, buffers.positions);
		gl.vertexAttribPointer(
			programInfo.attribLocations.vertexPosition,
			numComponents,
			type,
			normalize,
			stride,
			offset);
		gl.enableVertexAttribArray(
			programInfo.attribLocations.vertexPosition);
	}


	{
		const numComponents = 3;
		const type = gl.FLOAT;
		const normalize = false;
		const stride = 0;
		const offset = 0;
		gl.bindBuffer(gl.ARRAY_BUFFER, buffers.normals);
		gl.vertexAttribPointer(
			programInfo.attribLocations.vertexNormal,
			numComponents,
			type,
			normalize,
			stride,
			offset);
		gl.enableVertexAttribArray(
			programInfo.attribLocations.vertexNormal);
	}

	// tell webgl how to pull out the texture coordinates from buffer
	{
		const num = 2; // every coordinate composed of 2 values
		const type = gl.FLOAT; // the data in the buffer is 32 bit float
		const normalize = false; // don't normalize
		const stride = 0; // how many bytes to get from one set to the next
		const offset = 0; // how many bytes inside the buffer to start from
		gl.bindBuffer(gl.ARRAY_BUFFER, buffers.uvs);
		gl.vertexAttribPointer(programInfo.attribLocations.textureCoord,
			num,
			type,
			normalize,
			stride,
			offset);
		gl.enableVertexAttribArray(programInfo.attribLocations.textureCoord);
	}

	// Tell WebGL which indices to use to index the vertices
	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers.indices);

	// Tell WebGL to use our program when drawing
	gl.useProgram(programInfo.program);
	// Set the shader uniforms
	gl.uniformMatrix4fv(
		programInfo.uniformLocations.projectionMatrix,
		false,
		projectionMatrix);
	gl.uniformMatrix4fv(
		programInfo.uniformLocations.viewMatrix,
		false,
		viewMatrix);
	gl.uniformMatrix4fv(
		programInfo.uniformLocations.modelMatrix,
		false,
		modelMatrix);
	gl.uniformMatrix4fv(
		programInfo.uniformLocations.normalModelMatrix,
		false,
		normalModelMatrix);

	// Tell WebGL we want to affect texture unit 0
	gl.activeTexture(gl.TEXTURE0);
	// Bind the texture to texture unit 0
	gl.bindTexture(gl.TEXTURE_2D, texture);
	// Tell the shader we bound the texture to texture unit 0
	gl.uniform1i(programInfo.uniformLocations.uSampler, 0);

	{
		const vertexCount = length;
		const type = gl.UNSIGNED_SHORT;
		const offset = 0;
		gl.drawElements(gl.TRIANGLES, vertexCount, type, offset);
	}

	squareRotation += deltaTime * params.rotSpeed;
}
