/****************************************************************************
**
** Copyright (C) 2015 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing/
**
** This file is part of the QtCanvas3D module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL3$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see http://www.qt.io/terms-conditions. For further
** information use the contact form at http://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPLv3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or later as published by the Free
** Software Foundation and appearing in the file LICENSE.GPL included in
** the packaging of this file. Please review the following information to
** ensure the GNU General Public License version 2.0 requirements will be
** met: http://www.gnu.org/licenses/gpl-2.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

Qt.include("gl-matrix.js")
Qt.include("ThreeJSLoader.js")

var gl;

var texturedShaderProgram = 0;
var vertexShader = 0;
var fragmentShader = 0;

var vertexPositionAttribute;
var textureCoordAttribute;
var vertexNormalAttribute;

var pMatrixUniform;
var mvMatrixUniform;
var nMatrixUniform;
var textureSamplerUniform;

var barrelTexture = 0;

var mvMatrix = mat4.create();
var pMatrix  = mat4.create();
var nMatrix  = mat4.create();
var startTime;

var canvas3d;
var isLogEnabled = false;

function log(message) {
    if (isLogEnabled)
        console.log(message)
}

function Model() {
    this.verticesVBO = 0;
    this.normalsVBO  = 0;
    this.texCoordVBO = 0;
    this.indexVBO    = 0;
    this.count       = 0;
}

var theModel = new Model();

function initializeGL(canvas) {
    canvas3d = canvas
    log("*******************************************************************************************")
    log("initializeGL ENTER...")
    try {
        startTime = Date.now();

        // Get the OpenGL context jsonObj that represents the API we call
        log("Getting Context");
        gl = canvas.getContext("canvas3d", {depth:true, antialias:true});
        log("Context received "+gl);

        var contextConfig = gl.getContextAttributes();
        log("Depth: "+contextConfig.alpha);
        log("Stencil: "+contextConfig.stencil);
        log("Antialiasing: "+contextConfig.antialias);
        log("Premultiplied alpha: "+contextConfig.premultipliedAlpha);
        log("Preserve drawingbuffer: "+contextConfig.preserveDrawingBuffer);
        log("Prefer Low Power To High Performance: "+contextConfig.preferLowPowerToHighPerformance);
        log("Fail If Major Performance Caveat: "+contextConfig.failIfMajorPerformanceCaveat);

        // Setup the OpenGL state
        gl.enable(gl.DEPTH_TEST);
        gl.enable(gl.CULL_FACE);

        gl.frontFace(gl.CCW);
        gl.cullFace(gl.BACK);

        gl.disable(gl.BLEND);

        gl.clearColor(0.98, 0.98, 0.98, 1.0);
        gl.clearDepth(1.0);

        // Set viewport
        gl.viewport(0, 0,
                    canvas.width * canvas.devicePixelRatio,
                    canvas.height * canvas.devicePixelRatio);

        // Initialize the shader program
        initShaders();

        // Initialize buffers
        theModel.verticesVBO = gl.createBuffer();
        theModel.verticesVBO.name = "BarrelModel.verticesVBO";
        theModel.normalsVBO = gl.createBuffer();
        theModel.normalsVBO.name = "BarrelModel.normalsVBO";
        theModel.texCoordVBO = gl.createBuffer();
        theModel.texCoordVBO.name = "BarrelModel.texCoordVBO";
        theModel.indexVBO = gl.createBuffer();
        theModel.indexVBO.name = "BarrelModel.indexVBO";

        // Load the barrel texture
        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
        // Load the Qt logo as texture
        var barrelImage = TextureImageFactory.newTexImage();
        barrelImage.imageLoaded.connect(function() {
            barrelTexture = gl.createTexture();
            barrelTexture.name = "barrelTexture"
            gl.bindTexture(gl.TEXTURE_2D, barrelTexture);
            gl.texImage2D(gl.TEXTURE_2D,    // target
                          0,                // level
                          gl.RGBA,          // internalformat
                          gl.RGBA,          // format
                          gl.UNSIGNED_BYTE, // type
                          barrelImage);     // pixels

            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
            gl.generateMipmap(gl.TEXTURE_2D);
        });
        barrelImage.imageLoadingFailed.connect(function() {
            console.log("Texture load FAILED, "+barrelImage.errorString);
        });
        // Aliasing doesn't work correctly with QtQuick compiler for some reason,
        // so use full names for barrel.jpg and barrel.json
        barrelImage.src = "qrc:/qml/interaction/barrel.jpg";

        // Load the model
        log("    Create XMLHttpRequest")
        var request = new XMLHttpRequest();
        log("    XMLHttpRequest.open")
        request.open("GET", "qrc:/qml/interaction/barrel.json");
        log("    XMLHttpRequest.onreadystatechange")
        request.onreadystatechange = function () {
            if (request.readyState === XMLHttpRequest.DONE) {
                handleLoadedModel(JSON.parse(request.responseText));
            }
        }
        log("    XMLHttpRequest.send")
        request.send();

        log("...initializeGL EXIT");
    } catch(e) {
        console.log("...initializeGL FAILURE!");
        console.log(""+e);
        console.log(""+e.message);
    }
    log("*******************************************************************************************");
}

function resizeGL(canvas)
{
    var pixelRatio = canvas.devicePixelRatio;
    canvas.pixelSize = Qt.size(canvas.width * pixelRatio,
                               canvas.height * pixelRatio);
    if (gl)
        gl.viewport(0, 0,
                    canvas.width * canvas.devicePixelRatio,
                    canvas.height * canvas.devicePixelRatio);
}

function paintGL(canvas) {
    // draw
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    // draw only when we have the mesh and texture
    if (theModel.count <= 0 || barrelTexture == 0) return;

    gl.useProgram(texturedShaderProgram);

    // Calculate the perspective projection
    mat4.perspective(pMatrix, degToRad(45), canvas.width / canvas.height, 0.1, 10000.0);
    gl.uniformMatrix4fv(pMatrixUniform, false, pMatrix);

    // Bind the correct buffers
    gl.bindBuffer(gl.ARRAY_BUFFER, theModel.verticesVBO);
    gl.enableVertexAttribArray(vertexPositionAttribute);
    gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, theModel.normalsVBO);
    gl.enableVertexAttribArray(vertexNormalAttribute);
    gl.vertexAttribPointer(vertexNormalAttribute, 3, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, theModel.texCoordVBO);
    gl.enableVertexAttribArray(textureCoordAttribute);
    gl.vertexAttribPointer(textureCoordAttribute, 2, gl.FLOAT, false, 0, 0);

    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, barrelTexture);
    gl.uniform1i(textureSamplerUniform, 0);

    // Calculate and apply the modelview matrix
    mvMatrix = mat4.identity(mvMatrix);
    mvMatrix = mat4.translate(mvMatrix, mvMatrix, [0, -40, -700]);
    //! [0]
    mvMatrix = mat4.rotate(mvMatrix, mvMatrix, degToRad(canvas.xRotSlider), [1, 0, 0]);
    mvMatrix = mat4.rotate(mvMatrix, mvMatrix, degToRad(canvas.yRotSlider), [0, 1, 0]);
    mvMatrix = mat4.rotate(mvMatrix, mvMatrix, degToRad(canvas.zRotSlider), [0, 0, 1]);
    //! [0]
    gl.uniformMatrix4fv(mvMatrixUniform, false, mvMatrix);

    // Calculate normal matrix
    nMatrix = mat4.invert(nMatrix, mvMatrix);
    nMatrix = mat4.transpose(nMatrix, nMatrix);
    gl.uniformMatrix4fv(nMatrixUniform, false, nMatrix);

    // Draw the barrel
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, theModel.indexVBO);
    //! [1]
    gl.drawElements(gl.TRIANGLES, theModel.count, gl.UNSIGNED_SHORT, 0);
    //! [1]

    // Calculate and apply the modelview matrix
    mvMatrix = mat4.identity(mvMatrix);
    mvMatrix = mat4.translate(mvMatrix, mvMatrix, [-250, -50, -700]);
    mvMatrix = mat4.rotate(mvMatrix, mvMatrix, degToRad(canvas.xRotSlider), [0, 1, 0]);
    gl.uniformMatrix4fv(mvMatrixUniform, false, mvMatrix);

    // Calculate normal matrix
    nMatrix = mat4.invert(nMatrix, mvMatrix);
    nMatrix = mat4.transpose(nMatrix, nMatrix);
    gl.uniformMatrix4fv(nMatrixUniform, false, nMatrix);

    // Draw the barrel
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, theModel.indexVBO);
    //! [2]
    gl.drawElements(gl.POINTS, theModel.count, gl.UNSIGNED_SHORT, 0);
    //! [2]

    // Calculate and apply the modelview matrix
    mvMatrix = mat4.identity(mvMatrix);
    mvMatrix = mat4.translate(mvMatrix, mvMatrix, [250, -50, -700]);
    mvMatrix = mat4.rotate(mvMatrix, mvMatrix, degToRad(canvas.zRotSlider), [0, 1, 0]);
    gl.uniformMatrix4fv(mvMatrixUniform, false, mvMatrix);

    // Calculate normal matrix
    nMatrix = mat4.invert(nMatrix, mvMatrix);
    nMatrix = mat4.transpose(nMatrix, nMatrix);
    gl.uniformMatrix4fv(nMatrixUniform, false, nMatrix);

    // Draw the barrel
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, theModel.indexVBO);
    //! [3]
    gl.drawElements(gl.LINES, theModel.count, gl.UNSIGNED_SHORT, 0);
    //! [3]
}

function handleLoadedModel(jsonObj) {
    log("*******************************************************************************************");
    log("handleLoadedModel ENTER...")
    var modelData = parseJSON3DModel(jsonObj, "");

    log("    "+theModel.verticesVBO);
    gl.bindBuffer(gl.ARRAY_BUFFER, theModel.verticesVBO);
    gl.bufferData(gl.ARRAY_BUFFER,
                  new Float32Array(modelData.vertices),
                  gl.STATIC_DRAW);

    log("    "+theModel.normalsVBO);
    gl.bindBuffer(gl.ARRAY_BUFFER, theModel.normalsVBO);
    gl.bufferData(gl.ARRAY_BUFFER,
                  new Float32Array(modelData.normals),
                  gl.STATIC_DRAW);

    log("    "+theModel.texCoordVBO);
    gl.bindBuffer(gl.ARRAY_BUFFER, theModel.texCoordVBO);
    gl.bufferData(gl.ARRAY_BUFFER,
                  new Float32Array(modelData.texCoords[0]),
                  gl.STATIC_DRAW);

    log("    "+theModel.indexVBO);
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, theModel.indexVBO);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,
                  new Uint16Array(modelData.indices),
                  gl.STATIC_DRAW);

    theModel.count = modelData.indices.length;
    log("...handleLoadedModel EXIT");
    log("*******************************************************************************************");
}

function degToRad(degrees) {
    return degrees * Math.PI / 180;
}

function initShaders()
{
    log("    initShaders ENTER...")

    vertexShader = getShader(gl,
                             "attribute highp vec3 aVertexNormal;       \
                              attribute highp vec3 aVertexPosition;     \
                              attribute highp vec2 aTextureCoord;       \
                                                                        \
                              uniform highp mat4 uNormalMatrix;         \
                              uniform mat4 uMVMatrix;                   \
                              uniform mat4 uPMatrix;                    \
                                                                        \
                              varying highp vec2 vTextureCoord;         \
                              varying highp vec3 vLighting;             \
                                                                        \
                              void main(void) {                         \
                                 gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); \
                                 vTextureCoord = aTextureCoord;                                   \
                                 highp vec3 ambientLight = vec3(0.5, 0.5, 0.5);                   \
                                 highp vec3 directionalLightColor = vec3(0.75, 0.75, 0.75);       \
                                 highp vec3 directionalVector = vec3(0.85, 0.8, 0.75);            \
                                 highp vec4 transformedNormal = uNormalMatrix * vec4(aVertexNormal, 1.0); \
                                                                                                          \
                                 highp float directional = max(dot(transformedNormal.xyz, directionalVector), 0.0); \
                                 vLighting = ambientLight + (directionalLightColor * directional);                  \
                                 gl_PointSize = 1.0; \
                             }", gl.VERTEX_SHADER);

    fragmentShader = getShader(gl,
                               "varying highp vec2 vTextureCoord;             \
                                varying highp vec3 vLighting;                 \
                                                                              \
                                uniform sampler2D uSampler;                   \
                                                                              \
                                void main(void) {                             \
                                    mediump vec4 texelColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));  \
                                    gl_FragColor = vec4(texelColor.rgb * vLighting, 1.0);                                   \
                                }", gl.FRAGMENT_SHADER);

    texturedShaderProgram = gl.createProgram();
    texturedShaderProgram.name = "TexturedShaderProgram";
    gl.attachShader(texturedShaderProgram, vertexShader);
    gl.attachShader(texturedShaderProgram, fragmentShader);
    gl.linkProgram(texturedShaderProgram);

    if (!gl.getProgramParameter(texturedShaderProgram, gl.LINK_STATUS)) {
        console.log("Could not initialize shaders");
        console.log(gl.getProgramInfoLog(texturedShaderProgram));
    }

    gl.useProgram(texturedShaderProgram);

    // look up where the vertex data needs to go.
    vertexPositionAttribute = gl.getAttribLocation(texturedShaderProgram, "aVertexPosition");
    vertexPositionAttribute.name = "aVertexPosition_AttribLocation";
    gl.enableVertexAttribArray(vertexPositionAttribute);
    vertexNormalAttribute = gl.getAttribLocation(texturedShaderProgram, "aVertexNormal");
    vertexPositionAttribute.name = "aVertexNormal_AttribLocation";
    gl.enableVertexAttribArray(vertexNormalAttribute);
    textureCoordAttribute = gl.getAttribLocation(texturedShaderProgram, "aTextureCoord");
    vertexPositionAttribute.name = "aTextureCoord_AttribLocation";
    gl.enableVertexAttribArray(textureCoordAttribute);

    pMatrixUniform  = gl.getUniformLocation(texturedShaderProgram, "uPMatrix");
    pMatrixUniform.name = "uPMatrix_UniformLocation";
    mvMatrixUniform = gl.getUniformLocation(texturedShaderProgram, "uMVMatrix");
    mvMatrixUniform.name = "uMVMatrix_UniformLocation";
    textureSamplerUniform = gl.getUniformLocation(texturedShaderProgram, "uSampler")
    textureSamplerUniform.name = "uSampler_UniformLocation";
    nMatrixUniform = gl.getUniformLocation(texturedShaderProgram, "uNormalMatrix");
    nMatrixUniform.name = "uNormalMatrix_UniformLocation";
    log("    ... initShaders EXIT");
}

function getShader(gl, str, type) {
    var shader = gl.createShader(type);
    gl.shaderSource(shader, str);
    gl.compileShader(shader);

    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        console.log("JS:Shader compile failed");
        console.log(gl.getShaderInfoLog(shader));
        return null;
    }

    return shader;
}
