"use strict";

const v3 = {
  add: function (v1, v2) {
    return [v1[0] + v2[0], v1[1] + v2[1], v1[2] + v2[2]];
  },
  normalize: function (v) {
    return v3.scale(v, 1 / v3.norm(v));
  },
  norm: function (v) {
    return Math.sqrt(Math.pow(v[0], 2) + Math.pow(v[1], 2) + Math.pow(v[2], 2));
  },
  scale: function (v, f) {
    return [v[0] * f, v[1] * f, v[2] * f];
  },
};

function getGyroPermission(){
  if(!DeviceOrientationEvent.requestPermission){
    return Promise.resolve("granted")
  }
  else{
    return DeviceOrientationEvent.requestPermission()
  }
}

function createShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    throw new Error(gl.getShaderInfoLog(shader));
  }

  return shader;
}

function createProgram(gl, shaders) {
  const program = gl.createProgram();
  for (const shader of shaders) {
    gl.attachShader(program, shader);
  }
  gl.linkProgram(program);

  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    throw new Error(gl.getProgramInfoLog(program));
  }

  return program;
}

function initIsocahedron(vertices, elements) {
  const t = 1 + Math.sqrt(5) / 2;

  vertices.push(
    v3.normalize([-1, t, 0]),
    v3.normalize([1, t, 0]),
    v3.normalize([-1, -t, 0]),
    v3.normalize([1, -t, 0]),
    v3.normalize([0, -1, t]),
    v3.normalize([0, 1, t]),
    v3.normalize([0, -1, -t]),
    v3.normalize([0, 1, -t]),
    v3.normalize([t, 0, -1]),
    v3.normalize([t, 0, 1]),
    v3.normalize([-t, 0, -1]),
    v3.normalize([-t, 0, 1]),
  );

  elements.push(
    0, 11, 5,
    0, 5, 1,
    0, 1, 7,
    0, 7, 10,
    0, 10, 11,
    1, 5, 9,
    5, 11, 4,
    11, 10, 2,
    10, 7, 6,
    7, 1, 8,
    3, 9, 4,
    3, 4, 2,
    3, 2, 6,
    3, 6, 8,
    3, 8, 9,
    4, 9, 5,
    2, 4, 11,
    6, 2, 10,
    8, 6, 7,
    9, 8, 1,
  );
}

function getMidpoint(vertices, midpoints, i0, i1) {
  const k = `${i0}_${i1}`;
  if (midpoints.has(k)) {
    return vertices[midpoints[k]];
  }

  const p0 = vertices[i0];
  const p1 = vertices[i1];

  const midpoint = v3.normalize(v3.scale(v3.add(p0, p1), 0.5));
  const midpointIndex = vertices.length;
  midpoints.set(k, midpointIndex);
  vertices.push(midpoint);
  return midpointIndex;
}

function subdivideIsocahedron(vertices, elements, midpoints) {
  const newElements = [];
  for (let i = 0; i < elements.length; i += 3) {
    const p0 = elements[i];
    const p1 = elements[i + 1];
    const p2 = elements[i + 2];
    const m0 = getMidpoint(vertices, midpoints, p0, p1);
    const m1 = getMidpoint(vertices, midpoints, p1, p2);
    const m2 = getMidpoint(vertices, midpoints, p2, p0);

    newElements.push(
      m0, m1, m2,
      p0, m0, m2,
      p1, m1, m0,
      p2, m2, m1
    );
  }
  return newElements;
}

function generateIsocahedron() {
  const r = 1;
  const n = 3;
  const tau = 2 * Math.PI;

  const vertices = [];
  const normals = [];
  let elements = [];

  initIsocahedron(vertices, elements);
  const midpoints = new Map();
  for (let i = 0; i < 1; i++) {
    elements = subdivideIsocahedron(vertices, elements, midpoints);
  }

  return {
    vertices: [].concat(...vertices),
    // as we want to approximate a unit sphere, all the vertices are unit vectors
    normals: [].concat(...vertices),
    elements
  };
}

const canvas = document.querySelector("#canvas");
const gl = canvas.getContext("webgl");

const vertexShader = createShader(gl, gl.VERTEX_SHADER, `
  attribute vec4 a_position;
  attribute vec3 a_normal;

  uniform mat4 u_matrix;
  uniform mat4 u_world;
  uniform mat4 u_worldIT;
  uniform vec3 u_light;
  uniform vec3 u_camera;

  varying vec3 v_normal;
  varying vec3 v_surfaceToLight;
  varying vec3 v_surfaceToView;
  varying vec3 v_pos;

  void main() {
    gl_Position = u_matrix * a_position;
    v_pos = a_position.rgb;
    v_pos.r = abs(v_pos.r);
    v_pos.g = abs(v_pos.g);
    v_pos.b = abs(v_pos.b);

    v_normal = mat3(u_worldIT) * a_normal;

    vec3 surfaceWorldPos = (u_world * a_position).xyz;
    v_surfaceToLight = u_light - surfaceWorldPos;

    v_surfaceToView = u_camera - surfaceWorldPos;
  }
`);

const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, `
  precision highp float;

  varying vec3 v_normal;
  varying vec3 v_surfaceToLight;
  varying vec3 v_surfaceToView;
  varying vec3 v_pos;

  void main() {
    vec3 halfVector = normalize(normalize(v_surfaceToLight) + normalize(v_surfaceToView));

    float ka = 0.3;
    float kd = 0.8;
    float ks = 0.6;
    float gamma = 100.0;

    gl_FragColor = vec4(v_pos, 1.0);
    gl_FragColor.rgb *= ka + (kd * dot(normalize(v_normal), normalize(v_surfaceToLight)));
    gl_FragColor.rgb += ks * pow(max(0.0, dot(normalize(v_normal), halfVector)), gamma);
  }
`);

const program = createProgram(gl, [vertexShader, fragmentShader]);

const {vertices, normals, elements} = generateIsocahedron();

const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

const normalBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);

const elementBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(elements), gl.STATIC_DRAW);

gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);

const positionAttribLocation = gl.getAttribLocation(program, "a_position");
const normalAttribLocation = gl.getAttribLocation(program, "a_normal");

const matrixLocation = gl.getUniformLocation(program, "u_matrix");
const worldMatrixLocation = gl.getUniformLocation(program, "u_world");
const worldITMatrixLocation = gl.getUniformLocation(program, "u_worldIT");
const lightVecLocation = gl.getUniformLocation(program, "u_light");
const cameraVecLocation = gl.getUniformLocation(program, "u_camera");

const aspectRatio = gl.canvas.width / gl.canvas.height;
const zNear = 1;
const zFar = 2000;

const rotation = [0, 0, 0];

gl.useProgram(program);

gl.enableVertexAttribArray(positionAttribLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(
  positionAttribLocation,
  3, // element size
  gl.FLOAT,
  false, // normalize
  0, // stride
  0 // offset
);

gl.enableVertexAttribArray(normalAttribLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
gl.vertexAttribPointer(normalAttribLocation, 3, gl.FLOAT, false, 0, 0);

gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementBuffer);

function drawScene () {
  window.requestAnimationFrame(drawScene);

  const t = performance.now() / 1000;

  const lightPos = [10, 10, 30];
  const projectionMatrix = m4.perspective(Math.PI / 180 * 60, aspectRatio, zNear, zFar);
  const cameraPos = [0, 0, 30];
  const target = [0, 0, 0];
  const up = [0, 1, 0];
  const cameraMatrix = m4.lookAt(cameraPos, target, up);
  const viewMatrix = m4.inverse(cameraMatrix);
  let worldMatrix = m4.identity();
  worldMatrix = m4.xRotate(worldMatrix, rotation[0]);
  worldMatrix = m4.yRotate(worldMatrix, rotation[1]);
  worldMatrix = m4.zRotate(worldMatrix, rotation[2]);
  worldMatrix = m4.scale(worldMatrix, 12, 12, 12);

  const worldITMatrix = m4.transpose(m4.inverse(worldMatrix));
  const viewProjectionMatrix = m4.multiply(projectionMatrix, viewMatrix);
  const worldViewProjectionMatrix = m4.multiply(viewProjectionMatrix, worldMatrix);

  /*
  let matrix = projectionMatrix;
  matrix = m4.translate(matrix, 0, 0, -30);
  matrix = m4.xRotate(matrix, rotation[0]);
  matrix = m4.yRotate(matrix, rotation[1]);
  matrix = m4.zRotate(matrix, rotation[2]);
  matrix = m4.scale(matrix, 10, 10, 10);
  */
  gl.uniformMatrix4fv(matrixLocation, false, worldViewProjectionMatrix);
  gl.uniformMatrix4fv(worldMatrixLocation, false, worldMatrix);
  gl.uniformMatrix4fv(worldITMatrixLocation, false, worldITMatrix);
  gl.uniform3fv(lightVecLocation, lightPos);
  gl.uniform3fv(cameraVecLocation, cameraPos);

  gl.clearColor(0, 0, 0, 1);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

  gl.drawElements(
    gl.TRIANGLES,
    elements.length, // count
    gl.UNSIGNED_SHORT,
    0, // offset
  );
}

drawScene();

canvas.addEventListener("click", e => {
  getGyroPermission()
  .then(response => {
    window.addEventListener("deviceorientation", e => {
      rotation[0] = 2 * (-e.beta / 180 * Math.PI);
      rotation[1] = 2 * (-e.gamma / 180 * Math.PI);
      rotation[2] = 2 * (-e.alpha / 180 * Math.PI + Math.PI / 2);
    });
  });
});
