/*
 * Copyright (c) 2012 The Chromium Authors. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *    * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *    * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

// The "model" matrix is the "world" matrix in Standard Annotations
// and Semantics
var model = new Matrix4x4();
var view = new Matrix4x4();
var projection = new Matrix4x4();

var controller = null;

var bumpReflectVertexSource = [
    "attribute vec3 g_Position;",
    "attribute vec3 g_TexCoord0;",
    "attribute vec3 g_Tangent;",
    "attribute vec3 g_Binormal;",
    "attribute vec3 g_Normal;",
    "",
    "uniform mat4 world;",
    "uniform mat4 worldInverseTranspose;",
    "uniform mat4 worldViewProj;",
    "uniform mat4 viewInverse;",
    "",
    "varying vec2 texCoord;",
    "varying vec3 worldEyeVec;",
    "varying vec3 worldNormal;",
    "varying vec3 worldTangent;",
    "varying vec3 worldBinorm;",
    "",
    "void main() {",
    "  gl_Position = worldViewProj * vec4(g_Position.xyz, 1.);",
    "  texCoord.xy = g_TexCoord0.xy;",
    "  worldNormal = (worldInverseTranspose * vec4(g_Normal, 1.)).xyz;",
    "  worldTangent = (worldInverseTranspose * vec4(g_Tangent, 1.)).xyz;",
    "  worldBinorm = (worldInverseTranspose * vec4(g_Binormal, 1.)).xyz;",
    "  vec3 worldPos = (world * vec4(g_Position, 1.)).xyz;",
    "  worldEyeVec = normalize(worldPos - viewInverse[3].xyz);",
    "}"
    ].join("\n");

var bumpReflectFragmentSource = [
    "precision mediump float;\n",
    "const float bumpHeight = 0.2;",
    "",
    "uniform sampler2D normalSampler;",
    "uniform samplerCube envSampler;",
    "",
    "varying vec2 texCoord;",
    "varying vec3 worldEyeVec;",
    "varying vec3 worldNormal;",
    "varying vec3 worldTangent;",
    "varying vec3 worldBinorm;",
    "",
    "void main() {",
    "  vec2 bump = (texture2D(normalSampler, texCoord.xy).xy * 2.0 - 1.0) * bumpHeight;",
    "  vec3 normal = normalize(worldNormal);",
    "  vec3 tangent = normalize(worldTangent);",
    "  vec3 binormal = normalize(worldBinorm);",
    "  vec3 nb = normal + bump.x * tangent + bump.y * binormal;",
    "  nb = normalize(nb);",
    "  vec3 worldEye = normalize(worldEyeVec);",
    "  vec3 lookup = reflect(worldEye, nb);",
    "  vec4 color = textureCube(envSampler, lookup);",
    "  gl_FragColor = color;",
    "}"
    ].join("\n");

var wireVertexSource = [
    "attribute vec3 g_Position;",
    "",
    "uniform mat4 worldViewProj;",
    "",
    "void main() {",
    "  gl_Position = worldViewProj * vec4(g_Position.xyz, 1.);",
    "}"
    ].join("\n");

var wireFragmentSource = [
    "precision mediump float;\n",
    "",
    "void main() {",
    "  gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);",
    "}"
    ].join("\n");

function log(msg) {
    if (window.console && window.console.log) {
        console.log(msg);
    }
}

function output(str) {
    document.body.appendChild(document.createTextNode(str));
    document.body.appendChild(document.createElement("br"));
}

function checkGLError(gl) {
    var error = gl.getError();
    if (error != gl.NO_ERROR && error != gl.CONTEXT_LOST_WEBGL) {
        var str = "GL Error: " + error;
        output(str);
        throw str;
    }
}

function main() {
    var highDpiDemo = new TeapotDemo(document.getElementById("c1"));
    var lowDpiDemo = new TeapotDemo(document.getElementById("c2"));

    controller = new CameraController(document.body);

    controller.onchange = function(xRot, yRot) {
        highDpiDemo.draw();
        lowDpiDemo.draw();
    };

    highDpiDemo.startDpiAwareDemo();
    lowDpiDemo.startDemo();
}

function TeapotDemo(canvas) {
    this.canvas = canvas;
    this.gl = null;
    this.bumpTexture = null;
    this.envTexture = null;
    this.programObject = null;
    this.wireProgramObject = null;
    this.vbo = null;
    this.elementVbo = null;
    this.wireElementVbo = null;
    this.normalsOffset = 0;
    this.tangentsOffset = 0;
    this.binormalsOffset = 0;
    this.texCoordsOffset = 0;
    this.numElements = 0;
    this.numWireElements = 0;

    // Uniform variables
    this.worldLoc = 0;
    this.worldInverseTransposeLoc = 0;
    this.worldViewProjLoc = 0;
    this.viewInverseLoc = 0;
    this.normalSamplerLoc = 0;
    this.envSamplerLoc = 0;
    this.wireWorldViewProjLoc = 0;

    // Array of images curently loading
    this.loadingImages = [];
    this.pendingTextureLoads = 0;

    // Lost context handling
    var self = this;

    this.canvas.addEventListener('webglcontextlost', function(e) {
        log("handle context lost");
        e.preventDefault();
        self.clearLoadingImages();
    }, false);

    this.canvas.addEventListener('webglcontextrestored', function(e) {
        log("handle context restored");
        self.init();
    }, false);
}

TeapotDemo.prototype.startDemo = function() {
    this.canvas.width = this.canvas.clientWidth;
    this.canvas.height = this.canvas.clientHeight;
    this.init();
}

TeapotDemo.prototype.startDpiAwareDemo = function() {
    try {
        let resizeObserver = new ResizeObserver(entries => {
            for (let entry of entries) {
                let devicePixelSize = entry.devicePixelContentBoxSize[0];
                this.resize(devicePixelSize.inlineSize, devicePixelSize.blockSize);
            }
        });
        resizeObserver.observe(this.canvas, { box: "device-pixel-content-box" });
    } catch (e) {
        // device-pixel-content-box (or ResizeObserver altogether) is
        // not supported on this browser. Fall back to the old
        // algorithm which doesn't handle resizing.
        let ratio = window.devicePixelRatio ? window.devicePixelRatio : 1;
        this.resize(this.canvas.clientWidth * ratio, this.canvas.clientHeight * ratio);
    }

    this.init();
}

TeapotDemo.prototype.resize = function(newWidth, newHeight) {
    this.canvas.width = newWidth;
    this.canvas.height = newHeight;
}

TeapotDemo.prototype.init = function() {
    // Antialiasing is disabled to magnify the effects of the Higher DPI settings
    var gl = WebGLUtils.setupWebGL(this.canvas, {antialias: false});
    if (!gl)
        return;
    this.gl = gl;

    gl.enable(gl.DEPTH_TEST);
    gl.clearColor(0.0, 0.0, 0.0, 0.0);

    this.initTeapot(gl);
    this.initShaders(gl);
    this.bumpTexture = this.loadTexture(gl, "bump.jpg");
    this.envTexture = this.loadCubeMap(gl, "skybox", "jpg");
    this.draw();
}

TeapotDemo.prototype.initTeapot = function(gl) {
    this.vbo = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo);
    gl.bufferData(gl.ARRAY_BUFFER,
                  teapotPositions.byteLength +
                  teapotNormals.byteLength +
                  teapotTangents.byteLength +
                  teapotBinormals.byteLength +
                  teapotTexCoords.byteLength,
                  gl.STATIC_DRAW);
    this.normalsOffset = teapotPositions.byteLength;
    this.tangentsOffset = this.normalsOffset + teapotNormals.byteLength;
    this.binormalsOffset = this.tangentsOffset + teapotTangents.byteLength;
    this.texCoordsOffset = this.binormalsOffset + teapotBinormals.byteLength;
    gl.bufferSubData(gl.ARRAY_BUFFER, 0, teapotPositions);
    gl.bufferSubData(gl.ARRAY_BUFFER, this.normalsOffset, teapotNormals);
    gl.bufferSubData(gl.ARRAY_BUFFER, this.tangentsOffset, teapotTangents);
    gl.bufferSubData(gl.ARRAY_BUFFER, this.binormalsOffset, teapotBinormals);
    gl.bufferSubData(gl.ARRAY_BUFFER, this.texCoordsOffset, teapotTexCoords);

    this.elementVbo = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.elementVbo);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, teapotIndices, gl.STATIC_DRAW);
    this.numElements = teapotIndices.length;

    var wireIndices = this.createWireIndicies(teapotIndices);
    this.wireElementVbo = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.wireElementVbo);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, wireIndices, gl.STATIC_DRAW);
    this.numWireElements = wireIndices.length;
    checkGLError(gl);

}

TeapotDemo.prototype.createWireIndicies = function(triIndicies) {
    var wireIndices = new Uint16Array(triIndicies.length * 2);
    var i, j;
    for(i = 0, j = 0; i < triIndicies.length; i += 3) {
        wireIndices[j++] = triIndicies[i];
        wireIndices[j++] = triIndicies[i+1];

        wireIndices[j++] = triIndicies[i+1];
        wireIndices[j++] = triIndicies[i+2];

        wireIndices[j++] = triIndicies[i+2];
        wireIndices[j++] = triIndicies[i];
    }
    return wireIndices;
}

TeapotDemo.prototype.loadShader = function(gl, type, shaderSrc) {
    var shader = gl.createShader(type);
    // Load the shader source
    gl.shaderSource(shader, shaderSrc);
    // Compile the shader
    gl.compileShader(shader);
    // Check the compile status
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS) &&
        !gl.isContextLost()) {
        var infoLog = gl.getShaderInfoLog(shader);
        output("Error compiling shader:\n" + infoLog);
        gl.deleteShader(shader);
        return null;
    }
    return shader;
}

TeapotDemo.prototype.initShaders = function(gl) {
    var vertexShader = this.loadShader(gl, gl.VERTEX_SHADER, bumpReflectVertexSource);
    var fragmentShader = this.loadShader(gl, gl.FRAGMENT_SHADER, bumpReflectFragmentSource);
    // Create the program object
    var programObject = gl.createProgram();
    gl.attachShader(programObject, vertexShader);
    gl.attachShader(programObject, fragmentShader);
    // Bind attributes
    gl.bindAttribLocation(programObject, 0, "g_Position");
    gl.bindAttribLocation(programObject, 1, "g_TexCoord0");
    gl.bindAttribLocation(programObject, 2, "g_Tangent");
    gl.bindAttribLocation(programObject, 3, "g_Binormal");
    gl.bindAttribLocation(programObject, 4, "g_Normal");
    // Link the program
    gl.linkProgram(programObject);
    // Check the link status
    var linked = gl.getProgramParameter(programObject, gl.LINK_STATUS);
    if (!linked && !gl.isContextLost()) {
        var infoLog = gl.getProgramInfoLog(programObject);
        output("Error linking program:\n" + infoLog);
        gl.deleteProgram(programObject);
        return;
    }

    this.programObject = programObject;
    // Look up uniform locations
    this.worldLoc = gl.getUniformLocation(programObject, "world");
    this.worldInverseTransposeLoc = gl.getUniformLocation(programObject, "worldInverseTranspose");
    this.worldViewProjLoc = gl.getUniformLocation(programObject, "worldViewProj");
    this.viewInverseLoc = gl.getUniformLocation(programObject, "viewInverse");
    this.normalSamplerLoc = gl.getUniformLocation(programObject, "normalSampler");
    this.envSamplerLoc = gl.getUniformLocation(programObject, "envSampler");
    checkGLError(gl);

    vertexShader = this.loadShader(gl, gl.VERTEX_SHADER, wireVertexSource);
    fragmentShader = this.loadShader(gl, gl.FRAGMENT_SHADER, wireFragmentSource);
    // Create the program object
    programObject = gl.createProgram();
    gl.attachShader(programObject, vertexShader);
    gl.attachShader(programObject, fragmentShader);
    // Bind attributes
    gl.bindAttribLocation(programObject, 0, "g_Position");
    // Link the program
    gl.linkProgram(programObject);
    // Check the link status
    var linked = gl.getProgramParameter(programObject, gl.LINK_STATUS);
    if (!linked && !gl.isContextLost()) {
        var infoLog = gl.getProgramInfoLog(programObject);
        output("Error linking program:\n" + infoLog);
        gl.deleteProgram(programObject);
        return;
    }
    this.wireProgramObject = programObject;
    this.wireWorldViewProjLoc = gl.getUniformLocation(programObject, "worldViewProj");
}

TeapotDemo.prototype.draw = function() {
    var gl = this.gl;

    // Handle resizes by setting the viewport each time a render is performed.
    gl.viewport(0, 0, this.canvas.width, this.canvas.height);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    // For now, don't render if we have incomplete textures, just to
    // avoid accidentally incurring OpenGL errors -- although we should
    // be fully able to load textures in in the background
    if (this.pendingTextureLoads > 0) {
        return;
    }

    // Set up the model, view and projection matrices
    projection.loadIdentity();
    projection.perspective(45, this.canvas.width / this.canvas.height, 10, 500);
    view.loadIdentity();
    view.translate(0, -10, -100.0);

    // Add in camera controller's rotation
    model.loadIdentity();
    model.rotate(controller.xRot, 1, 0, 0);
    model.rotate(controller.yRot, 0, 1, 0);

    // Correct for initial placement and orientation of model
    model.translate(0, -10, 0);
    model.rotate(90, 1, 0, 0);

    gl.enable(gl.POLYGON_OFFSET_FILL);
    gl.polygonOffset(1, -1);

    gl.useProgram(this.programObject);

    // Compute necessary matrices
    var mvp = new Matrix4x4();
    mvp.multiply(model);
    mvp.multiply(view);
    mvp.multiply(projection);
    var worldInverseTranspose = model.inverse();
    worldInverseTranspose.transpose();
    var viewInverse = view.inverse();

    // Set up uniforms
    gl.uniformMatrix4fv(this.worldLoc, gl.FALSE, new Float32Array(model.elements));
    gl.uniformMatrix4fv(this.worldInverseTransposeLoc, gl.FALSE, new Float32Array(worldInverseTranspose.elements));
    gl.uniformMatrix4fv(this.worldViewProjLoc, gl.FALSE, new Float32Array(mvp.elements));
    gl.uniformMatrix4fv(this.viewInverseLoc, gl.FALSE, new Float32Array(viewInverse.elements));
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, this.bumpTexture);
    gl.uniform1i(this.normalSamplerLoc, 0);
    gl.activeTexture(gl.TEXTURE1);
    gl.bindTexture(gl.TEXTURE_CUBE_MAP, this.envTexture);
    gl.uniform1i(this.envSamplerLoc, 1);

    // Bind and set up vertex streams
    gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo);
    gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(0);
    gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, this.texCoordsOffset);
    gl.enableVertexAttribArray(1);
    gl.vertexAttribPointer(2, 3, gl.FLOAT, false, 0, this.tangentsOffset);
    gl.enableVertexAttribArray(2);
    gl.vertexAttribPointer(3, 3, gl.FLOAT, false, 0, this.binormalsOffset);
    gl.enableVertexAttribArray(3);
    gl.vertexAttribPointer(4, 3, gl.FLOAT, false, 0, this.normalsOffset);
    gl.enableVertexAttribArray(4);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.elementVbo);
    gl.drawElements(gl.TRIANGLES, this.numElements, gl.UNSIGNED_SHORT, 0);

    // Render the mesh wireframe
    gl.useProgram(this.wireProgramObject);

    gl.uniformMatrix4fv(this.wireWorldViewProjLoc, gl.FALSE, new Float32Array(mvp.elements));

    gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(0);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.wireElementVbo);
    gl.drawElements(gl.LINES, this.numWireElements, gl.UNSIGNED_SHORT, 0);
}

// Clears all the images currently loading.
// This is used to handle context lost events.
TeapotDemo.prototype.clearLoadingImages = function() {
    for (var ii = 0; ii < this.loadingImages.length; ++ii) {
        this.loadingImages[ii].onload = undefined;
    }
    this.loadingImages = [];
}

TeapotDemo.prototype.loadTexture = function(gl, src) {
    var texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
    ++this.pendingTextureLoads;
    var self = this;
    var image = new Image();
    this.loadingImages.push(image);
    image.onload = function() {
        self.loadingImages.splice(self.loadingImages.indexOf(image), 1);
        --self.pendingTextureLoads;
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
        gl.texImage2D(
            gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
        checkGLError(gl);
        self.draw();
    };
    image.src = src;
    return texture;
}

TeapotDemo.prototype.loadCubeMap = function(gl, base, suffix) {
    var texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture);
    checkGLError(gl);
    gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    checkGLError(gl);
    gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    checkGLError(gl);
    // FIXME: TEXTURE_WRAP_R doesn't exist in OpenGL ES?!
    //  gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE);
    //  checkGLError();
    gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    checkGLError(gl);
    gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    checkGLError(gl);
    var faces = [["posx", gl.TEXTURE_CUBE_MAP_POSITIVE_X],
                 ["negx", gl.TEXTURE_CUBE_MAP_NEGATIVE_X],
                 ["posy", gl.TEXTURE_CUBE_MAP_POSITIVE_Y],
                 ["negy", gl.TEXTURE_CUBE_MAP_NEGATIVE_Y],
                 ["posz", gl.TEXTURE_CUBE_MAP_POSITIVE_Z],
                 ["negz", gl.TEXTURE_CUBE_MAP_NEGATIVE_Z]];
    var self = this;
    for (var i = 0; i < faces.length; i++) {
        var url = base + "-" + faces[i][0] + "." + suffix;
        var face = faces[i][1];
        ++this.pendingTextureLoads;
        var image = new Image();
        this.loadingImages.push(image);
        // Javascript has function, not block, scope.
        // See "JavaScript: The Good Parts", Chapter 4, "Functions",
        // section "Scope".
        image.onload = function(texture, face, image, url) {
            return function() {
                self.loadingImages.splice(self.loadingImages.indexOf(image), 1);
                --self.pendingTextureLoads;
                gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture);
                gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
                gl.texImage2D(
                   face, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
                checkGLError(gl);
                self.draw();
            }
        }(texture, face, image, url);
        image.src = url;
    }
    return texture;
}
