Source: examples/lighting/content/Lighting2.js

/**
 * @file
 *
 * Summary.
 * <p>Lighting and shading models: <a href="https://en.wikipedia.org/wiki/Lambertian_reflectance">Lambert</a>  x
 * <a href="https://en.wikipedia.org/wiki/Phong_reflection_model">Phong</a>.  </p>
 *
 * Here, we add a {@link getModelData function} to take a model created by {@link https://threejs.org three.js}
 * and extract the data for vertices and normals, <br>
 * so we can load it directly to the GPU.
 *
 * Edit {@link mainEntrance} to select a {@link selectModel model} and {@link makeCube} to set
 * {@link https://www.scratchapixel.com/lessons/3d-basic-rendering/introduction-to-shading/shading-normals face or vertex normals}.
 *
 * @author {@link https://stevekautz.com Steve Kautz}
 * @author modified by {@link https://krotalias.github.io Paulo Roma}
 * @since 27/09/2016
 * @see <a href="https://dl.acm.org/doi/pdf/10.1145/362736.362739">Flat shading</a> -
 * <a href="https://csl.illinois.edu/news-and-media/tech-reports">Wendell Jack Bouknight</a> (1970)
 * @see <a href="https://ohiostate.pressbooks.pub/app/uploads/sites/45/2017/09/gouraud1971.pdf">Gouraud shading</a> -
 * <a href="https://en.wikipedia.org/wiki/Henri_Gouraud_(computer_scientist)">Henri Gouraud</a> (1971)
 * @see <a href="https://dl.acm.org/doi/pdf/10.1145/360825.360839">Phong shading</a> -
 * <a href="https://en.wikipedia.org/wiki/Bui_Tuong_Phong">Bui Tuong Phong</a> (1975)
 * @see <a href="https://cg.cs.tsinghua.edu.cn/course/docs/chap1%20final.pdf">Computer Graphics Survey</a>
 * @see <a href="/cwdc/13-webgl/examples/lighting/content/Lighting2.js">source</a>
 * @see <a href="/cwdc/13-webgl/examples/lighting/content/Lighting2.html">Lambert diffuse model, Gouraud shading</a>
 * @see <a href="/cwdc/13-webgl/examples/lighting/content/Lighting2a.html">Lambert diffuse model, Phong shading</a>
 * @see <a href="/cwdc/13-webgl/examples/lighting/content/Lighting2b.html">Phong reflection model, Gouraud shading</a>
 * @see <a href="/cwdc/13-webgl/examples/lighting/content/Lighting2c.html">Phong reflection model, Phong shading</a>
 * @see <img width="512" src="/cwdc/13-webgl/examples/lighting/content/Lighting.png">
 */

"use strict";

// CDN always works
//import * as THREE from "https://unpkg.com/three@0.148.0/build/three.module.js?module";
//import { TeapotGeometry } from "https://unpkg.com/three@0.148.0/examples/jsm/geometries/TeapotGeometry.js?module";

// importmap does not work on safari and IOS
//import * as THREE from "three";
//import { TeapotGeometry } from "TeapotGeometry";

import * as THREE from "/cwdc/13-webgl/lib/three.module.js";
import { TeapotGeometry } from "/cwdc/13-webgl/lib/TeapotGeometry.js";

/**
 * Three.js module.
 * @author Ricardo Cabello ({@link https://coopermrdoob.weebly.com/ Mr.doob})
 * @since 24/04/2010
 * @external three
 * @see {@link https://threejs.org/docs/#manual/en/introduction/Installation Installation}
 * @see {@link https://discoverthreejs.com DISCOVER three.js}
 * @see {@link https://riptutorial.com/ebook/three-js Learning three.js}
 * @see {@link https://github.com/mrdoob/three.js github}
 * @see {@link http://cindyhwang.github.io/interactive-design/Mrdoob/index.html An interview with Mr.doob}
 * @see {@link https://experiments.withgoogle.com/search?q=Mr.doob Experiments with Google}
 */

/**
 * <p>Main three.js namespace.</p>
 * {@link event:load Imported} from {@link external:three three.module.js}
 * @namespace THREE
 * @see {@link https://stackoverflow.com/questions/68528251/three-js-error-during-additional-components-importing Three.js ERROR during additional components importing}
 * @see {@link https://dplatz.de/blog/2019/es6-bare-imports.html How to handle ES6 bare module imports for local Development}
 */

/**
 * <p>A representation of mesh, line, or point geometry.</p>
 * Includes vertex positions, face indices, normals, colors, UVs,
 * and custom attributes within buffers, reducing the cost of
 * passing all this data to the GPU.
 * @class BufferGeometry
 * @memberof THREE
 * @see {@link https://threejs.org/docs/#api/en/core/BufferGeometry BufferGeometry}
 */

/**
 * Axis coordinates.
 * @type {Float32Array}
 */
// prettier-ignore
const axisVertices = new Float32Array([
  0.0, 0.0, 0.0,
  1.5, 0.0, 0.0,
  0.0, 0.0, 0.0,
  0.0, 1.5, 0.0,
  0.0, 0.0, 0.0,
  0.0, 0.0, 1.5,
]);

/**
 * Axis colors.
 * @type {Float32Array}
 */
// prettier-ignore
const axisColors = new Float32Array([
  1.0, 0.0, 0.0, 1.0,
  1.0, 0.0, 0.0, 1.0,
  0.0, 1.0, 0.0, 1.0,
  0.0, 1.0, 0.0, 1.0,
  0.0, 0.0, 1.0, 1.0,
  0.0, 0.0, 1.0, 1.0,
]);

// A few global variables...

/**
 * The OpenGL context.
 * @type {WebGLRenderingContext}
 */
let gl;

/**
 * Our model data.
 * @type {modelData}
 */
let theModel;

/**
 * Array with normal end points.
 * @type {Float32Array}
 */
let normal;

/**
 * Array with edges end points.
 * @type {Float32Array}
 */
let lines;

/**
 * Handle to a buffer on the GPU.
 * @type {WebGLBuffer}
 */
let vertexBuffer;
/**  @type {WebGLBuffer} */
let indexBuffer;
/**  @type {WebGLBuffer} */
let vertexNormalBuffer;
/**  @type {WebGLBuffer} */
let axisBuffer;
/**  @type {WebGLBuffer} */
let axisColorBuffer;
/**  @type {WebGLBuffer} */
let normalBuffer;

/**
 * Handle to a buffer on the GPU.
 * @type {WebGLBuffer}
 */
let lineBuffer;

/**
 * Handle to the compiled shader program on the GPU.
 * @type {WebGLShader}
 */
let lightingShader;

/**
 * Handle to the compiled shader program on the GPU.
 * @type {WebGLShader}
 */
let colorShader;

/**
 * Model transformation matrix.
 * @Type {Matrix4}
 */
let modelMatrix = new Matrix4();

/**
 * Current rotation axis.
 * @type {String}
 */
let axis = "x";

/**
 * Scale applied to a model to make its size adequate for rendering.
 * @type {Number}
 */
let mscale = 1;

/**
 * Turn the display of the model mesh/texture/axes/animation on/off.
 * @type {Object}
 * @property {Boolean} lines mesh visible/invisible
 * @property {Boolean} texture model surface visible/invisible
 * @property {Boolean} axes axes visible/invisible
 * @property {Boolean} paused animation on/off
 */
const selector = {
  lines: false,
  texture: true,
  axes: false,
  paused: true,
};

/**
 * Arcball.
 * @type {SimpleRotator}
 */
let rotator;

/**
 * Camera position.
 * @type {Array<Number>}
 */
const eye = [1.77, 3.54, 3.06];

/**
 * <p>View matrix.</p>
 * One strategy is to identify a transformation to our
 * <a href="https://cglearn.codelight.eu/pub/computer-graphics/frames-of-reference-and-projection">camera</a>
 * <a href="/cwdc/downloads/Computer%20Graphics%20-%20CMSC%20427.pdf#page=31">frame</a>
 * then invert it.  <br>
 * Therefore, the camera transformation takes (0,0,0) to the <span style="color:red">camera position.</span>
 * <pre>
 *    rotate(30, 0, 1, 0) * rotate(-45, 1, 0, 0) * translate(0, 0, 5)
 *
 *    camera transformation:
 *        0.8660253882408142 -0.3535533845424652 0.3535533845424652 <span style="color:red">1.7677669525146484</span>
 *        0                   0.7071067690849304 0.7071067690849304 <span style="color:red">3.535533905029297</span>
 *       -0.5                -0.6123723983764648 0.6123723983764648 <span style="color:red">3.061861991882324</span>
 *        0                   0                   0                 1
 *
 *    translate(0, 0, -5) * rotate(45, 1, 0, 0) * rotate(-30, 0, 1, 0)
 *
 *    view matrix:
 *        0.8660253882408142 0                  -0.5                 0
 *       -0.3535533845424652 0.7071067690849304 -0.6123723983764648  0
 *        0.3535533845424652 0.7071067690849304  0.6123723983764648 -5
 *        0                  0                   0                   1
 *
 * </pre>
 * The view matrix is the inverse of the camera's transformation matrix: viewMatrix = camera ⁻¹.
 * <ul>
 * <li>The camera's transformation matrix takes something that's local to the camera
 * and transforms it to world space <br>
 * (transforming the point [0,0,0] will give you the camera's position)</li>
 * <li>The view matrix takes something that's in world space and transforms it
 * so that it's local to the camera <br>
 * (transforming the camera's position will give you [0, 0, 0])</li>
 * </ul>
 * <p><a href="https://www.geertarien.com/blog/2017/07/30/breakdown-of-the-lookAt-function-in-OpenGL/">LookAt</a>
 * functions from math <a href="https://dens.website/tutorials/webgl/gl-matrix">libraries</a> are just a convenience, indeed,
 * and requires a view point,
 * a point to look at, and a direction "up", for camera orientation:</p>
 * <p>The approximate {@link eye view point} here is: <span style="color:red">[1.77, 3.54, 3.06]</span></p>
 *
 * <pre>
 *    const viewMatrix = new {@link Matrix4 Matrix4()}.setLookAt(
 *      ...eye,   // view point
 *      0, 0, 0,  // at - looking at the origin
 *      0, 1, 0); // up vector - y axis
 *
 * or using the {@link https://glmatrix.net glmatrix} package
 *
 *    const viewMatrix = {@link https://glmatrix.net/docs/module-mat4.html mat4}.lookAt(
 *      [],         // mat4 frustum matrix will be written into
 *      eye,        // view point
 *      [0, 0, 0],  // look at (center)
 *      [0, 1, 0]); // view up
 * </pre>
 * @type {Matrix4}
 * @see <a href="/cwdc/downloads/apostila.pdf#page=109">View matrix</a>
 * @see <a href="/cwdc/downloads/PDFs/06_LCG_Transformacoes.pdf">Mudança de Base</a>
 * @see <a href="https://en.wikipedia.org/wiki/Change_of_basis">Change of Basis</a>
 * @see <a href="/cwdc/10-html5css3/hw2/doc-hw2">node</a>
 * @see {@link https://learn.microsoft.com/en-us/windows/win32/direct3d9/view-transform View Transform (Direct3D 9)}
 * @see {@link https://learn.microsoft.com/en-us/windows/win32/direct3d9/projection-transform Projection Transform (Direct3D 9)}
 * @see <img src="../projection.png" width="256"> <img src="../projection2.png" width="256">
 */
const viewMatrix = new Matrix4()
  .translate(0, 0, -5)
  .rotate(45, 1, 0, 0)
  .rotate(-30, 0, 1, 0);

/**
 * Returns the magnitude (length) of a vector.
 * @param {Array<Number>} v n-D vector.
 * @returns {Number} vector length.
 * @see {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce Array.prototype.reduce()}
 */
const vecLen = (v) =>
  Math.sqrt(v.reduce((accumulator, value) => accumulator + value * value, 0));

/**
 * View distance.
 * @type {Number}
 */
const viewDistance = vecLen(eye);

/**
 * <p>For projection, we can either use:
 * <ul>
 * <li>An orthographic projection, specifying
 * the clipping volume explicitly:
 *  <ul>
 *    <li>const <b>projection</b> = new Matrix4().setOrtho(-1.5, 1.5, -1, 1, 4, 6);</li>
 *  </ul>
 * </li>
 *
 * <li>Or the same perspective projection, using the Frustum function with:
 *  <ul>
 *    <li>a 30 degree field of view, and a near plane at 4,<br>
 *    which corresponds to a view plane height of: 4 * tan(15) = 1.07</li>
 *    <li>const <b>projection</b> = new Matrix4().setFrustum(-1.5 * 1.07, 1.5 * 1.07, -1.07, 1.07, 4, 6);</li>
 *  </ul>
 * </li>
 *
 * <li>Or a perspective projection specified with a
 * field of view, an aspect ratio, and distance to near and far
 * clipping planes:
 *  <ul>
 *    <li>const <b>projection</b> = new Matrix4().setPerspective(30, 1.5, 0.1, 1000);</li>
 *  </ul>
 * </ul>
 * Here use aspect ratio 3/2 corresponding to canvas size 900 x 600</p>
 * @type {Matrix4}
 */
const projection = new Matrix4().setPerspective(30, 1.5, 0.1, 1000);

/**
 * Loads the {@link mainEntrance application}.
 * @event load
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event Window: load event}
 */
window.addEventListener("load", (event) => mainEntrance());

/**
 * Draws the mesh and vertex normals, by generating an "l" {@link handleKeyPress event},
 * whenever the Mesh button is clicked.
 * @event clickMesh
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event Element: click event}
 * @see {@link createEvent}
 */
document
  .querySelector("#btnMesh")
  .addEventListener("click", (event) => handleKeyPress(createEvent("l")));

/**
 * Animates the object, by generating an " " {@link handleKeyPress event},
 * whenever the Rotate button is clicked.
 * @event clickRot
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event Element: click event}
 * @see {@link createEvent}
 */
document
  .querySelector("#btnRot")
  .addEventListener("click", (event) => handleKeyPress(createEvent(" ")));

/**
 * Animates the object, by generating an "↓" {@link handleKeyPress event},
 * whenever the Arrow Down button is clicked.
 * @event clickArrowDown
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event Element: click event}
 * @see {@link createEvent}
 */
document
  .querySelector("#arrowDown")
  .addEventListener("click", (event) =>
    handleKeyPress(createEvent("ArrowDown")),
  );

/**
 * Animates the object, by generating an "↑" {@link handleKeyPress event},
 * whenever the Arrow Up button is clicked.
 * @event clickArrowUp
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event Element: click event}
 * @see {@link createEvent}
 */
document
  .querySelector("#arrowUp")
  .addEventListener("click", (event) => handleKeyPress(createEvent("ArrowUp")));

/**
 * <p>Creates a unit cube, centered at the origin, and set its properties:
 * vertices, normal vectors, texture coordinates, indices and colors.</p>
 *
 * <p>For a proper specular reflection on planar faces, such as a cube or a polyhedron,
 * the normal vectors have to be perpendicular to the plane of each face.</p>
 *
 * <p>Computing an average normal, like is done here when creating indices,
 * is what one would want to do for a smooth object like a sphere.</p>
 * The resulting rendering is very unpleasant, in this case.
 * The right course is creating three duplicate vertices per cube corner.
 * <p>Even if cube.indices is not defined here, {@link drawModel} can handle it.</p>
 * @param {Boolean} create_indices whether to generated vertex indices or not.
 * @return {Object} cube
 * @property {Number} cube.numVertices number of vertices (36).
 * @property {Float32Array} cube.vertices vertex coordinate array (108 = 36 * 3).
 * @property {Float32Array} cube.normals vertex normal array (108 = 36 * 3).
 * @property {Float32Array} cube.colors vertex color array (144 = 36 * 4).
 * @property {Float32Array} cube.texCoords vertex texture array (72 = 36 * 2).
 * @property {Uint16Array} cube.indices vertex index array (36 = 6 * 2 * 3).
 * @see {@link THREE.BufferGeometry}
 * @see <img src="/cwdc/13-webgl/examples/images/cube.png">
 */
function makeCube(create_indices = false) {
  // 8 vertices of cube
  // prettier-ignore
  const rawVertices = new Float32Array([
    -0.5, -0.5,  0.5,  // v0
     0.5, -0.5,  0.5,  // v1
     0.5,  0.5,  0.5,  // v2
    -0.5,  0.5,  0.5,  // v3
    -0.5, -0.5, -0.5,  // v4
     0.5, -0.5, -0.5,  // v5
     0.5,  0.5, -0.5,  // v6
    -0.5,  0.5, -0.5,  // v7
  ]);

  // prettier-ignore
  const rawColors = new Float32Array([
      1.0, 0.0, 0.0, 1.0,  // red
      0.0, 1.0, 0.0, 1.0,  // green
      0.0, 0.0, 1.0, 1.0,  // blue
      1.0, 1.0, 0.0, 1.0,  // yellow
      1.0, 0.0, 1.0, 1.0,  // magenta
      0.0, 1.0, 1.0, 1.0,  // cyan
    ]);

  // 6 normals of the faces
  // prettier-ignore
  const rawNormals = new Float32Array([
     0,  0,  1,  // +z face
     1,  0,  0,  // +x face
     0,  0, -1,  // -z face
    -1,  0,  0,  // -x face
     0,  1,  0,  // +y face
     0, -1,  0,  // -y face
  ]);

  // 8 texture coordinates of the vertices
  // prettier-ignore
  const rawTexture = new Float32Array([
    0.0, 0.0,  // v0
    1.0, 0.0,  // v1
    1.0, 1.0,  // v2
    0.0, 1.0,  // v3
    0.0, 0.0,  // v4
    1.0, 0.0,  // v5
    1.0, 1.0,  // v6
    0.0, 1.0,  // v7
 ]);

  const n = 1 / Math.sqrt(3);
  // 8 normals of the vertices
  // prettier-ignore
  const rawVertexNormals = new Float32Array([
    -n, -n,  n,  // v0
     n, -n,  n,  // v1
     n,  n,  n,  // v2
    -n,  n,  n,  // v3
    -n, -n, -n,  // v4
     n, -n, -n,  // v5
     n,  n, -n,  // v6
    -n,  n, -n,  // v7
 ]);

  // prettier-ignore
  const indices = new Uint16Array([
      0, 1, 2, 0, 2, 3,  // +z face
      2, 1, 5, 6, 2, 5,  // +x face
      5, 4, 7, 5, 7, 6,  // -z face
      7, 4, 0, 0, 3, 7,  // -x face
      3, 2, 7, 2, 6, 7,  // +y face
      0, 5, 1, 0, 4, 5   // -y face
    ]);

  const verticesArray = [];
  const colorsArray = [];
  const normalsArray = [];
  const textureArray = [];
  for (let i = 0; i < 36; ++i) {
    // for each of the 36 vertices...
    let face = Math.floor(i / 6);
    let index = indices[i];

    // (x, y, z): three numbers for each point
    for (let j = 0; j < 3; ++j) {
      verticesArray.push(rawVertices[3 * index + j]);
    }

    // (r, g, b, a): four numbers for each point
    for (let j = 0; j < 4; ++j) {
      colorsArray.push(rawColors[4 * face + j]);
    }

    // (nx, ny, nz): three numbers for each point
    for (let j = 0; j < 3; ++j) {
      normalsArray.push(rawNormals[3 * face + j]);
    }

    // (tx, ty): two numbers for each point
    for (let j = 0; j < 2; ++j) {
      textureArray.push(rawTexture[2 * index + j]);
    }
  }

  return create_indices
    ? {
        numVertices: 8,
        vertexPositions: rawVertices, // 24 = 8 * 3
        vertexNormals: rawVertexNormals, // 24 = 8 * 3
        vertexTextureCoords: rawTexture, // 72 = 36 * 2
        indices: indices, // 36 = 6 faces * 2 tri/face  * 3 ind/tri
      }
    : {
        numVertices: 36, // 12 tri  * 3 vert/tri
        vertexPositions: new Float32Array(verticesArray), // 108 = 36 * 3
        vertexNormals: new Float32Array(normalsArray), // 108 = 36 * 3
        colors: new Float32Array(colorsArray), // 144 = 36 * 4
        vertexTextureCoords: new Float32Array(textureArray), // 72 = 36 * 2
        indices: new Uint16Array([...Array(36).keys()]), // 36 = 6 * 2 * 3
      };
}

/**
 * <p>Matrix for taking normals into eye space.</p>
 * Returns a matrix to transform normals, so they stay
 * perpendicular to surfaces after a linear transformation.
 * @param {Matrix4} model model matrix.
 * @param {Matrix4} view view matrix.
 * @return {Float32Array} 3x3 normal matrix (transpose inverse) from the 4x4 modelview matrix.
 * @see <a href="/cwdc/13-webgl/extras/doc/gdc12_lengyel.pdf#page=48">𝑛′=(𝑀<sup>&#8211;1</sup>)<sup>𝑇</sup>⋅𝑛</a>
 */
function makeNormalMatrixElements(model, view) {
  let n = new Matrix4(view).multiply(model);
  n.transpose();
  n.invert();
  n = n.elements;
  // prettier-ignore
  return new Float32Array([
        n[0], n[1], n[2],
        n[4], n[5], n[6],
        n[8], n[9], n[10],
    ]);
}

/**
 * Translate keypress events to strings.
 * @param {KeyboardEvent} event key pressed.
 * @return {String} typed character.
 * @see {@link https://javascript.info/tutorial/keyboard-events Keyboard: keydown and keyup}
 */
function getChar(event) {
  event = event || window.event;
  const charCode = event.key || String.fromCharCode(event.which);
  return charCode;
}

/**
 * <p>Closure for keydown events.</p>
 * Chooses a {@link theModel model} and which {@link axis} to rotate around.<br>
 * @param {KeyboardEvent} event keyboard event.
 * @function
 * @return {key_event} callback for handling a keyboard event.
 */
const handleKeyPress = ((event) => {
  const zoomfactor = 0.7;
  let gscale = 1;
  const models = document.getElementById("models");

  /**
   * <p>Handler for keydown events.</p>
   * @param {KeyboardEvent} event keyboard event.
   * @callback key_event callback to handle a key pressed.
   */
  return (event) => {
    const ch = getChar(event);
    switch (ch) {
      case " ":
        selector.paused = !selector.paused;
        animate();
        break;
      case "x":
      case "y":
      case "z":
        axis = ch;
        break;
      case "o":
        modelMatrix.setIdentity();
        rotator.setViewMatrix(modelMatrix.elements);
        mscale = gscale;
        axis = "x";
        break;
      case "l":
        selector.lines = !selector.lines;
        if (!selector.lines) selector.texture = true;
        break;
      case "k":
        selector.texture = !selector.texture;
        if (!selector.texture) selector.lines = true;
        break;
      case "a":
        selector.axes = !selector.axes;
        break;
      case "v":
        // cube
        gscale = mscale = 1;
        models.value = "2";
        theModel = createModel(makeCube());
        //theModel = createModel(getModelData(new THREE.BoxGeometry(1, 1, 1)));
        break;
      case "s":
        // sphere with more faces
        gscale = mscale = 1;
        models.value = "5";
        theModel = createModel(
          getModelData(new THREE.SphereGeometry(1, 48, 24)),
        );
        break;
      case "T":
        // torus knot
        gscale = mscale = 1;
        models.value = "8";
        theModel = createModel(
          getModelData(new THREE.TorusKnotGeometry(0.6, 0.24, 128, 16)),
          1,
        );
        break;
      case "d":
        // dodecahedron
        gscale = mscale = 1;
        models.value = "9";
        theModel = createModel(
          getModelData(new THREE.DodecahedronGeometry(1, 0)),
        );
        break;
      case "p":
        // teapot - this is NOT a manifold model - it is a model with borders!
        gscale = mscale = 0.8;
        models.value = "6";
        theModel = createModel(
          getModelData(new TeapotGeometry(1, 10, true, true, true, true, true)),
          null,
        );
        break;
      case "ArrowUp":
      case ">":
        // Up pressed
        mscale *= zoomfactor;
        mscale = Math.max(gscale * 0.1, mscale);
        break;
      case "ArrowDown":
      case "<":
        // Down pressed
        mscale /= zoomfactor;
        mscale = Math.min(gscale * 3, mscale);
        break;

      default:
        return;
    }
    if (selector.paused) draw();
  };
})();

/**
 * Returns a new keyboard event.
 * @param {String} key char code.
 * @returns {KeyboardEvent} a keyboard event.
 * @function
 */
const createEvent = (key) => {
  const code = key.charCodeAt();
  return new KeyboardEvent("keydown", {
    key: key,
    which: code,
    charCode: code,
    keyCode: code,
  });
};

/**
 * Selects a model from a menu.
 */
function selectModel() {
  const val = document.getElementById("models").value;
  const key = {
    2: "v", // cube
    5: "s", // sphere
    6: "p", // teapot
    8: "T", // knot
    9: "d", // dodecahedron
  };
  handleKeyPress(createEvent(key[val]));
}

window.selectModel = selectModel;

/**
 * <p>Code to actually render our geometry.</p>
 * Draw {@link drawAxes axes}, {@link drawModel model}, and {@link drawLines lines}.
 */
function draw() {
  // clear the framebuffer
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

  if (selector.axes) drawAxes();
  if (selector.texture) drawModel();
  if (selector.lines) drawLines();
}

/**
 * Returns a new scale model matrix, which applies mscale.
 * @returns {Matrix4} model matrix.
 */
function getModelMatrix() {
  let m = modelMatrix;
  if (mscale != 1) {
    m = new Matrix4(modelMatrix).scale(mscale, mscale, mscale);
  }
  return m;
}

/**
 * <p>Draws the model, by
 * using the {@link lightingShader}.</p>
 * If {@link theModel}.indices is defined, then calls
 * {@link https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/drawElements drawElements}.
 * Otherwise, {@link https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/drawArrays drawArrays}.
 * <p>Since three.js {@link https://sbcode.net/threejs/geometry-to-buffergeometry/ version 125},
 * THREE.Geometry was deprecated and replaced by
 * {@link THREE.BufferGeometry THREE.BufferGeometry},
 * which always define indices for efficiency.
 */
function drawModel() {
  // bind the shader
  gl.useProgram(lightingShader);

  // get the index for the a_Position attribute defined in the vertex shader
  const positionIndex = gl.getAttribLocation(lightingShader, "a_Position");
  if (positionIndex < 0) {
    console.log("Failed to get the storage location of a_Position");
    return;
  }

  const normalIndex = gl.getAttribLocation(lightingShader, "a_Normal");
  if (normalIndex < 0) {
    console.log("Failed to get the storage location of a_Normal");
    return;
  }

  // "enable" the a_position attribute
  gl.enableVertexAttribArray(positionIndex);
  gl.enableVertexAttribArray(normalIndex);

  // bind buffers for points
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  gl.vertexAttribPointer(positionIndex, 3, gl.FLOAT, false, 0, 0);
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexNormalBuffer);
  gl.vertexAttribPointer(normalIndex, 3, gl.FLOAT, false, 0, 0);

  // set uniform in shader for projection * view * model transformation
  let loc = gl.getUniformLocation(lightingShader, "model");
  gl.uniformMatrix4fv(loc, false, getModelMatrix().elements);
  loc = gl.getUniformLocation(lightingShader, "view");
  gl.uniformMatrix4fv(loc, false, viewMatrix.elements);
  loc = gl.getUniformLocation(lightingShader, "projection");
  gl.uniformMatrix4fv(loc, false, projection.elements);
  loc = gl.getUniformLocation(lightingShader, "normalMatrix");
  gl.uniformMatrix3fv(
    loc,
    false,
    makeNormalMatrixElements(modelMatrix, viewMatrix),
  );

  loc = gl.getUniformLocation(lightingShader, "lightPosition");
  gl.uniform4f(loc, 2.0, 4.0, 2.0, 1.0);

  gl.uniform4f(
    gl.getUniformLocation(lightingShader, "diffuseColor"),
    0.0,
    0.8,
    0.8,
    1.0,
  );

  if (theModel.indices) {
    gl.drawElements(
      gl.TRIANGLES,
      theModel.indices.length,
      theModel.indices.constructor === Uint32Array
        ? gl.UNSIGNED_INT
        : gl.UNSIGNED_SHORT,
      0,
    );
  } else {
    gl.drawArrays(gl.TRIANGLES, 0, theModel.vertexPositions.length / 3);
  }

  gl.bindBuffer(gl.ARRAY_BUFFER, null);
  gl.disableVertexAttribArray(positionIndex);
  gl.disableVertexAttribArray(normalIndex);
  gl.useProgram(null);
}

/**
 * <p>Draws the axes. </p>
 * Uses the colorShader.
 */
function drawAxes() {
  // bind the shader
  gl.useProgram(colorShader);

  // get the index for the a_Position attribute defined in the vertex shader
  const positionIndex = gl.getAttribLocation(colorShader, "a_Position");
  if (positionIndex < 0) {
    console.log("Failed to get the storage location of a_Position");
    return;
  }

  const colorIndex = gl.getAttribLocation(colorShader, "a_Color");
  if (colorIndex < 0) {
    console.log("Failed to get the storage location of a_Color");
    return;
  }

  // "enable" the a_position attribute
  gl.enableVertexAttribArray(positionIndex);
  gl.enableVertexAttribArray(colorIndex);

  // draw axes (not transformed by model transformation)
  gl.bindBuffer(gl.ARRAY_BUFFER, axisBuffer);
  gl.vertexAttribPointer(positionIndex, 3, gl.FLOAT, false, 0, 0);
  gl.bindBuffer(gl.ARRAY_BUFFER, axisColorBuffer);
  gl.vertexAttribPointer(colorIndex, 4, gl.FLOAT, false, 0, 0);

  // set transformation to projection * view only
  const loc = gl.getUniformLocation(colorShader, "transform");
  const transform = new Matrix4().multiply(projection).multiply(viewMatrix);
  gl.uniformMatrix4fv(loc, false, transform.elements);

  // draw axes
  gl.drawArrays(gl.LINES, 0, 6);

  // unbind shader and "disable" the attribute indices
  // (not really necessary when there is only one shader)
  gl.bindBuffer(gl.ARRAY_BUFFER, null);
  gl.disableVertexAttribArray(positionIndex);
  gl.disableVertexAttribArray(colorIndex);
  gl.useProgram(null);
}

/**
 * <p>Draws the mesh edges and normals. </p>
 * Uses the colorShader.
 */
function drawLines() {
  // bind the shader
  gl.useProgram(colorShader);
  const positionIndex = gl.getAttribLocation(colorShader, "a_Position");
  if (positionIndex < 0) {
    console.log("Failed to get the storage location of a_Position");
    return;
  }

  const a_color = gl.getAttribLocation(colorShader, "a_Color");
  if (a_color < 0) {
    console.log("Failed to get the storage location of a_Color");
    return;
  }
  gl.vertexAttrib4f(a_color, 1.0, 1.0, 0.0, 1.0);

  // "enable" the a_position attribute
  gl.enableVertexAttribArray(positionIndex);
  //  ------------ draw triangle borders
  // set transformation to projection * view * model
  const loc = gl.getUniformLocation(colorShader, "transform");
  const transform = new Matrix4()
    .multiply(projection)
    .multiply(viewMatrix)
    .multiply(getModelMatrix());
  gl.uniformMatrix4fv(loc, false, transform.elements);

  // draw edges
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  gl.vertexAttribPointer(positionIndex, 3, gl.FLOAT, false, 0, 0);
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
  // takes too long on mobile
  /*
    for (let i = 0; i < theModel.indices.length; i += 3) {
        // offset - two bytes per index (UNSIGNED_SHORT)
        gl.drawElements(gl.LINE_LOOP, 3, gl.UNSIGNED_SHORT, i * 2);
    }
    */

  // draw edges
  if (theModel.indices) {
    gl.bindBuffer(gl.ARRAY_BUFFER, lineBuffer);
    gl.vertexAttribPointer(positionIndex, 3, gl.FLOAT, false, 0, 0);
    gl.drawArrays(gl.LINES, 0, 2 * theModel.indices.length);
  } else {
    for (let i = 0; i < theModel.vertexPositions.length; i += 3) {
      gl.drawArrays(gl.LINE_LOOP, i, 3);
    }
  }

  // draw normals
  gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
  gl.vertexAttribPointer(positionIndex, 3, gl.FLOAT, false, 0, 0);
  gl.drawArrays(gl.LINES, 0, 2 * theModel.vertexPositions.length);

  gl.bindBuffer(gl.ARRAY_BUFFER, null);
  gl.disableVertexAttribArray(positionIndex);
  gl.useProgram(null);
}

/**
 * <p>Sets up all buffers for the given (triangulated) model (shape).</p>
 *
 * Uses the webgl vertex buffer, normal buffer, texture buffer and index buffer, created in {@link mainEntrance}.<br>
 * Then, binds each one of them as an array buffer and copies the corresponding shape array data to them.
 *
 * <p>Also, the Euler characteristic for the model is:</p>
 * <ul>
 *  <li>χ = 2 − 2g − b </li>
 * </ul>
 * for a surface with g handles and b boundaries.
 *
 * <p>The number of triangles must be even for a valid triangulation of the sphere:</p>
 * <ul>
 *  <li> V - E + T = 2 (sphere) </li>
 *  <li> V - E + T = 0 (torus) </li>
 * </ul>
 *
 * @param {modelData} shape a <a href="https://en.wikipedia.org/wiki/Boundary_representation">BREP</a> model
 *                    given as an <a href="https://math.hws.edu/graphicsbook/c3/s4.html">IFS</a>.
 * @param {Number | null} chi model <a href="https://en.wikipedia.org/wiki/Euler_characteristic">Euler Characteristic</a>.
 * @returns {modelData} shape.
 * @see {@link https://en.wikipedia.org/wiki/Platonic_solid Platonic solid}
 * @see {@link https://ocw.mit.edu/courses/18-965-geometry-of-manifolds-fall-2004/pages/lecture-notes/ Geometry Of Manifolds}
 * @see {@link https://nrich.maths.org/1384 Euler's Formula and Topology}
 * @see {@link https://math.stackexchange.com/questions/3571483/euler-characteristic-of-a-polygon-with-a-hole Euler characteristic of a polygon with a hole}
 */
function createModel(shape, chi = 2) {
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, shape.vertexPositions, gl.STATIC_DRAW);

  if (shape.indices) {
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, shape.indices, gl.STATIC_DRAW);
  }

  gl.bindBuffer(gl.ARRAY_BUFFER, vertexNormalBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, shape.vertexNormals, gl.STATIC_DRAW);

  const nv = shape.vertexPositions.length;
  normal = new Float32Array(6 * nv);
  for (let i = 0, k = 0; i < nv; i += 3, k += 6) {
    for (let j = 0; j < 3; j++) {
      normal[j + k] = shape.vertexPositions[i + j];
      normal[j + k + 3] =
        normal[j + k] + (0.1 / mscale) * shape.vertexNormals[i + j];
    }
  }

  // number of faces: ni / 3
  // number of edges: ni
  // number of endpoints: ni * 6
  if (shape.indices) {
    const ni = shape.indices.length;
    lines = new Float32Array(18 * ni);
    for (let i = 0, k = 0; i < ni; i += 3, k += 18) {
      for (let j = 0; j < 3; j++) {
        const v1 = shape.vertexPositions[shape.indices[i] * 3 + j];
        const v2 = shape.vertexPositions[shape.indices[i + 1] * 3 + j];
        const v3 = shape.vertexPositions[shape.indices[i + 2] * 3 + j];

        lines[j + k] = v1;
        lines[j + k + 3] = v2;

        lines[j + k + 6] = v2;
        lines[j + k + 9] = v3;

        lines[j + k + 12] = v3;
        lines[j + k + 15] = v1;
      }
    }

    gl.bindBuffer(gl.ARRAY_BUFFER, lineBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, lines, gl.STATIC_DRAW);
  }

  gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, normal, gl.STATIC_DRAW);

  const obj = document.getElementById("object");
  obj.innerHTML = "<b>Object:</b>";
  const faces = shape.indices
    ? shape.indices.length / 3
    : shape.vertexPositions.length / 9;
  let edges = (faces * 3) / 2;
  let vertices = faces / 2 + chi;
  let vertReal = shape.vertexPositions.length / 3;

  if (chi === null) {
    edges = `??`;
    vertices = `??`;
  }

  obj.innerHTML = `(${faces} ▲, ${edges} ―, ${vertices} •, ${vertReal} 🔴)`;

  return shape;
}

/**
 * <p>Entry point when page is loaded.</p>
 * Load all data into the buffers (just once) before proceeding.
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bufferData bufferData}
 */
function mainEntrance() {
  // retrieve <canvas> element
  const canvas = document.getElementById("theCanvas");

  /**
   * <p>Key handler.</p>
   * Calls {@link handleKeyPress} whenever any of these keys is pressed:
   * <ul>
   *  <li>Space</li>
   *  <li>x, y, z</li>
   *  <li>p, s, T, o</li>
   *  <li>a, k, l</li>
   * </ul>
   *
   * @event keydown
   * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event Element: keydown event}
   */
  addEventListener("keydown", (event) => {
    if (
      ["Space", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].indexOf(
        event.code,
      ) > -1
    ) {
      event.preventDefault();
    }
    handleKeyPress();
  });

  // get the rendering context for WebGL
  gl = canvas.getContext("webgl2");
  if (!gl) {
    console.log("Failed to get the rendering context for WebGL");
    return;
  }

  // load and compile the shader pair, using utility from the teal book
  let vshaderSource = document.getElementById("vertexColorShader").textContent;
  let fshaderSource = document.getElementById(
    "fragmentColorShader",
  ).textContent;
  if (!initShaders(gl, vshaderSource, fshaderSource)) {
    console.log("Failed to initialize shaders.");
    return;
  }
  colorShader = gl.program;
  gl.useProgram(null);

  // load and compile the shader pair, using utility from the teal book
  vshaderSource = document.getElementById("vertexLightingShader").textContent;
  fshaderSource = document.getElementById("fragmentLightingShader").textContent;
  if (!initShaders(gl, vshaderSource, fshaderSource)) {
    console.log("Failed to initialize shaders.");
    return;
  }
  lightingShader = gl.program;
  gl.useProgram(null);

  // buffer for vertex positions for triangles
  vertexBuffer = gl.createBuffer();
  indexBuffer = gl.createBuffer();
  if (!vertexBuffer) {
    console.log("Failed to create the buffer object");
    return;
  }

  // buffer for vertex normals
  vertexNormalBuffer = gl.createBuffer();
  if (!vertexNormalBuffer) {
    console.log("Failed to create the buffer object");
    return;
  }

  // axes
  axisBuffer = gl.createBuffer();
  normalBuffer = gl.createBuffer();
  lineBuffer = gl.createBuffer();
  if (!axisBuffer) {
    console.log("Failed to create the buffer object");
    return;
  }
  gl.bindBuffer(gl.ARRAY_BUFFER, axisBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, axisVertices, gl.STATIC_DRAW);

  // buffer for axis colors
  axisColorBuffer = gl.createBuffer();
  if (!axisColorBuffer) {
    console.log("Failed to create the buffer object");
    return;
  }
  gl.bindBuffer(gl.ARRAY_BUFFER, axisColorBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, axisColors, gl.STATIC_DRAW);

  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  // specify a fill color for clearing the framebuffer
  gl.clearColor(0.0, 0.2, 0.2, 1.0);

  gl.enable(gl.DEPTH_TEST);

  gl.enable(gl.CULL_FACE);
  gl.cullFace(gl.BACK);

  rotator = new SimpleRotator(canvas, animate);
  rotator.setViewMatrix(modelMatrix.elements);
  rotator.setViewDistance(0);

  // initial model
  selectModel();

  // start drawing!
  animate();
}

/**
 * A closure to define an animation loop.
 * @return {loop}
 * @function
 */
const animate = (() => {
  // increase the rotation by some amount, depending on the axis chosen
  const increment = 0.5;
  /** @type {Number} */
  let requestID = 0;
  const axes = {
    x: [1, 0, 0],
    y: [0, 1, 0],
    z: [0, 0, 1],
  };

  /**
   * Callback to keep drawing frames.
   * @callback loop
   * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame requestAnimationFrame}
   * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelAnimationFrame cancelAnimationFrame}
   */
  return () => {
    draw();
    if (requestID != 0) {
      cancelAnimationFrame(requestID);
      requestID = 0;
    }
    if (!selector.paused) {
      modelMatrix = new Matrix4()
        .setRotate(increment, ...axes[axis])
        .multiply(modelMatrix);
      rotator.setViewMatrix(modelMatrix.elements);
      // request that the browser calls animate() again "as soon as it can"
      requestID = requestAnimationFrame(animate);
    } else {
      modelMatrix.elements = rotator.getViewMatrix();
    }
  };
})();