/*
Copyright (c) 2019 The Khronos Group Inc.
Use of this source code is governed by an MIT-style license that can be
found in the LICENSE.txt file.
*/
NoOverOptimizeOnUniformArrayTester = (function(){

var vshader = [
    "attribute vec4 a_position;",
    "void main()",
    "{",
    "    gl_Position = a_position;",
    "}"
].join('\n');

var fshader_max = [
    "precision mediump float;",
    "uniform vec4 colora[$(maxUniformVectors)];",
    "void main()",
    "{",
    "    gl_FragColor = vec4(colora[$(usedUniformVector)]);",
    "}"
].join('\n');

var fshader_max_ab_ab = [
    "precision mediump float;",
    "uniform vec4 $(decl1);",
    "uniform vec4 $(decl2);",
    "void main()",
    "{",
    "gl_FragColor = vec4($(usage1) + $(usage2));",
    "}"
].join('\n');

// MaxInt32 is 2^32-1. We need +1 of that to test overflow conditions
var MaxInt32PlusOne = 4294967296;

function setupTests(gl) {
    var tests = [];
    var maxUniformVectors = gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS);

    // This test is to test drivers the have bugs related to optimizing
    // an array of uniforms when only 1 of those uniforms is used.
    tests.push({
        desc: "using last element",
        maxUniformVectors: maxUniformVectors,
        usedUniformVector: maxUniformVectors - 1,
        shader: "fshader-max",
        color: [0, 1, 0, 1],
        arrayName: "colora",
        extraName: "colorb",
    });
    tests.push({
        desc: "using first element",
        maxUniformVectors: maxUniformVectors,
        usedUniformVector: 0,
        shader: "fshader-max",
        color: [0, 1, 0, 1],
        arrayName: "colora",
        extraName: "colorb",
    });

    // Generate test shaders. We're trying to force the driver to
    // overflow from 1 array into the next if it optimizes. So for example if it was C
    //
    //   int big[4];
    //   int little[1];
    //   big[5] = 124;
    //
    // Would end up setting little[0] instead of big. Some drivers optimize
    // where if you only use say 'big[3]' it will actually only allocate just 1 element
    // for big.
    //
    // But, some drivers have a bug where the fact that they optimized big to 1 element
    // does not get passed down to glUniform so when setting the uniform 'big[3]' they
    // overwrite memory.
    //
    // If the driver crashes, yea. We found a bug. We can block the driver.
    // Otherwise we try various combinations so that setting 'little[0]' first
    // and then setting all elements of 'big' we hope it will overwrite 'little[0]'
    // which will show the bug and again we can block the driver.
    //
    // We don't know how the driver will order, in memory, the various uniforms
    // or for that matter we don't even know if they will be contiguous in memory
    // but to hopefully expose any bugs we try various combinations.
    //
    //    It could be the compiler orders uniforms alphabetically.
    //    It could be it orders them in order of declaration.
    //    It could be it orders them in order of usage.
    //
    // We also test using only first element of big or just the last element of big.
    //
    for (var nameOrder = 0; nameOrder < 2; ++nameOrder) {
        var name1 = nameOrder ? "colora" : "colorb";
        var name2 = nameOrder ? "colorb" : "colora";
        for (var last = 0; last < 2; ++last) {
            var usedUniformVector = last ? maxUniformVectors - 2 : 0;
            for (var declOrder = 0; declOrder < 2; ++declOrder) {
                var bigName    = declOrder ? name1 : name2;
                var littleName = declOrder ? name2 : name1;
                var decl1 = bigName + "[" + (maxUniformVectors - 1) + "]";
                var decl2 = littleName + "[1]";
                if (declOrder) {
                    var t = decl1;
                    decl1 = decl2;
                    decl2 = t;
                }
                for (var usageOrder = 0; usageOrder < 2; ++usageOrder) {
                    var usage1 = bigName + "[" + usedUniformVector + "]";
                    var usage2 = littleName + "[0]";
                    if (usageOrder) {
                        var t = usage1;
                        usage1 = usage2;
                        usage2 = t;
                    }
                    var fSrc = wtu.replaceParams(fshader_max_ab_ab, {
                        decl1: decl1,
                        decl2: decl2,
                        usage1: usage1,
                        usage2: usage2,
                    });
                    var desc = "testing: " + name1 + ":" + name2 + " using " + (last ? "last" : "first") +
                        " creating uniforms " + decl1 + " " + decl2 + " and accessing " + usage1 + " " + usage2;
                    tests.push({
                        desc: desc,
                        maxUniformVectors: maxUniformVectors - 1,
                        usedUniformVector: usedUniformVector,
                        source: fSrc,
                        color: [0, 0, 0, 1],
                        arrayName: bigName,
                        extraName: littleName,
                    });
                }
            }
        }
    }
    return tests;
};

function testUniformOptimizationIssues(test) {
    debug("");
    debug(test.desc);
    var fshader = test.source;
    if (!fshader) {
        fshader = wtu.replaceParams(fshader_max, test);
    }

    var consoleElem = document.getElementById("console");
    wtu.addShaderSource(
        consoleElem, "vertex shader", vshader);
    wtu.addShaderSource(
        consoleElem, "fragment shader", fshader);

    var program = wtu.loadProgram(gl, vshader, fshader);
    gl.useProgram(program);

    var colorbLocation = gl.getUniformLocation(program, test.extraName + "[0]");
    if (colorbLocation) {
        gl.uniform4fv(colorbLocation, [0, 1, 0, 0]);
    }

    // Ensure that requesting an array uniform past MaxInt32PlusOne returns no uniform
    var nameMaxInt32PlusOne = test.arrayName + "[" + (test.usedUniformVector + MaxInt32PlusOne) + "]";
    assertMsg(gl.getUniformLocation(program, nameMaxInt32PlusOne) === null,
        "Requesting " + nameMaxInt32PlusOne + " uniform should return a null uniform location");

    // Set just the used uniform
    var name = test.arrayName + "[" + test.usedUniformVector + "]";
    var uniformLocation = gl.getUniformLocation(program, name);
    gl.uniform4fv(uniformLocation, test.color);
    wtu.setupIndexedQuad(gl, 1);
    wtu.clearAndDrawIndexedQuad(gl, 1);
    wtu.checkCanvas(gl, [0, 255, 0, 255], "should be green");

    // Set all the unused uniforms
    var locations = [];
    var allRequiredUniformLocationsQueryable = true;
    for (var ii = 0; ii < test.maxUniformVectors; ++ii) {
        var name = test.arrayName + "[" + ii + "]";
        var uniformLocation = gl.getUniformLocation(program, name);
        locations.push(uniformLocation);
        if (ii == test.usedUniformVector) {
            continue;
        }
        // Locations > usedUnformVector may not exist.
        // Locations <= usedUniformVector MUST exist.
        if (ii <= test.usedUniformVector && (uniformLocation === undefined || uniformLocation === null)) {
            allRequiredUniformLocationsQueryable = false;
        }
        gl.uniform4fv(uniformLocation, [1, 0, 0, 1]);
    }
    if (allRequiredUniformLocationsQueryable) {
        testPassed("allRequiredUniformLocationsQueryable is true.");
    }
    else {
        testFailed("allRequiredUniformLocationsQueryable should be true. Was false.");
    }
    var positionLoc = gl.getAttribLocation(program, "a_position");
    wtu.setupIndexedQuad(gl, 1, positionLoc);
    wtu.clearAndDrawIndexedQuad(gl, 1);
    wtu.checkCanvas(gl, [0, 255, 0, 255], "should be green");

    // Check we can read & write each uniform.
    // Note: uniforms past test.usedUniformVector might not exist.
    for (var ii = 0; ii < test.maxUniformVectors; ++ii) {
        gl.uniform4fv(locations[ii], [ii + 4, ii + 2, ii + 3, ii + 1]);
    }

    var kEpsilon = 0.01;
    var isSame = function(v1, v2) {
        return Math.abs(v1 - v2) < kEpsilon;
    };

    for (var ii = 0; ii < test.maxUniformVectors; ++ii) {
        var location = locations[ii];
        if (location) {
            var value = gl.getUniform(program, locations[ii]);
            if (!isSame(value[0], ii + 4) ||
                !isSame(value[1], ii + 2) ||
                !isSame(value[2], ii + 3) ||
                !isSame(value[3], ii + 1)) {
                testFailed("location: " + ii + " was not correct value");
                break;
            }
        }
    }
}

function runOneTest(gl, test) {
    testUniformOptimizationIssues(test);
};

function runTests(gl, tests) {
    debug("");
    debug("Test drivers don't over optimize unused array elements");

    for (var ii = 0; ii < tests.length; ++ii) {
        runOneTest(gl, tests[ii]);
    }
};

return {
    setupTests : setupTests,
    runTests : runTests
};

}());
