<!--
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.
-->

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="../../../resources/js-test-style.css"/>
<script src="../../../js/js-test-pre.js"></script>
<script src="../../../js/pnglib.js"></script>
<script src="../../../js/webgl-test-utils.js"></script>

<script>
"use strict";
var wtu = WebGLTestUtils;
var gl = null;
var textureLoc = null;
var successfullyParsed = false;

//----------------------------------------------------------------------
// Harness

var testCases = [];

var DataMode = {
    IMAGE: 0,
    IMAGE_DATA: 1,

    NUM_HTML_MODES: 2,

    RAW_DATA: 2,

    // This must remain the last mode.
    NUM_MODES: 3
};

function init()
{
    description('Verify texImage2D and texSubImage2D code paths taking both HTML and user-specified data with all format/type combinations');

    var canvas = document.getElementById("example");
    gl = wtu.create3DContext(canvas);
    gl.disable(gl.DITHER);
    var program = wtu.setupTexturedQuad(gl);

    gl.clearColor(0,0,0,1);
    gl.clearDepth(1);
    gl.disable(gl.BLEND);

    textureLoc = gl.getUniformLocation(program, "tex");

    initializeTests();
}

function initializeTests()
{
    // Verify that uploading to packed pixel formats performs the
    // required conversion and associated loss of precision.
    for (var dataMode = 0; dataMode < DataMode.NUM_HTML_MODES; ++dataMode) {
        for (var useTexSubImage2D = 0; useTexSubImage2D < 2; ++useTexSubImage2D) {
            testCases.push({
                dataMode: dataMode,
                useTexSubImage2D: !!useTexSubImage2D,
                width: 256,
                height: 1,
                generator: generateOpaqueGrayscaleRamp,
                premultiplyAlpha: false,
                format: gl.RGBA,
                type: gl.UNSIGNED_BYTE,
                verifier: allChannelsIncreaseByNoMoreThan,
                threshold: 1,
                numOccurrences: 1,
                description: "RGBA/UNSIGNED_BYTE should maintain full precision of data"
            });
            testCases.push({
                dataMode: dataMode,
                useTexSubImage2D: !!useTexSubImage2D,
                width: 256,
                height: 1,
                generator: generateOpaqueGrayscaleRamp,
                premultiplyAlpha: false,
                format: gl.RGBA,
                type: gl.UNSIGNED_SHORT_4_4_4_4,
                verifier: allChannelsIncreaseByAtLeast,
                threshold: 15,
                numOccurrences: 10,
                description: "RGBA/UNSIGNED_SHORT_4_4_4_4 must drop low four bits of precision"
            });
            testCases.push({
                dataMode: dataMode,
                useTexSubImage2D: !!useTexSubImage2D,
                width: 256,
                height: 1,
                generator: generateOpaqueGrayscaleRamp,
                premultiplyAlpha: false,
                format: gl.RGBA,
                type: gl.UNSIGNED_SHORT_5_5_5_1,
                verifier: allChannelsIncreaseByAtLeast,
                threshold: 7,
                numOccurrences: 20,
                description: "RGBA/UNSIGNED_SHORT_5_5_5_1 must drop low three bits of precision"
            });
            testCases.push({
                dataMode: dataMode,
                useTexSubImage2D: !!useTexSubImage2D,
                width: 256,
                height: 1,
                generator: generateOpaqueGrayscaleRamp,
                premultiplyAlpha: false,
                format: gl.RGB,
                type: gl.UNSIGNED_BYTE,
                verifier: allChannelsIncreaseByNoMoreThan,
                threshold: 1,
                numOccurrences: 1,
                description: "RGB/UNSIGNED_BYTE should maintain full precision of data"
            });
            testCases.push({
                dataMode: dataMode,
                useTexSubImage2D: !!useTexSubImage2D,
                width: 256,
                height: 1,
                generator: generateOpaqueGrayscaleRamp,
                premultiplyAlpha: false,
                format: gl.RGB,
                type: gl.UNSIGNED_SHORT_5_6_5,
                verifier: allChannelsIncreaseByAtLeast,
                threshold: 3,
                numOccurrences: 20,
                description: "RGB/UNSIGNED_SHORT_5_6_5 must drop low two or three bits of precision"
            });
            testCases.push({
                dataMode: dataMode,
                useTexSubImage2D: !!useTexSubImage2D,
                width: 256,
                height: 1,
                generator: generateTranslucentGrayscaleRamp,
                premultiplyAlpha: false,
                format: gl.ALPHA,
                type: gl.UNSIGNED_BYTE,
                verifier: alphaChannelIncreasesByNoMoreThan,
                threshold: 1,
                numOccurrences: 1,
                description: "ALPHA/UNSIGNED_BYTE should maintain full precision of data"
            });
            testCases.push({
                dataMode: dataMode,
                useTexSubImage2D: !!useTexSubImage2D,
                width: 256,
                height: 1,
                generator: generateOpaqueGrayscaleRamp,
                premultiplyAlpha: false,
                format: gl.LUMINANCE,
                type: gl.UNSIGNED_BYTE,
                verifier: allChannelsIncreaseByNoMoreThan,
                threshold: 1,
                numOccurrences: 1,
                description: "LUMINANCE/UNSIGNED_BYTE should maintain full precision of data"
            });
            testCases.push({
                dataMode: dataMode,
                useTexSubImage2D: !!useTexSubImage2D,
                width: 256,
                height: 1,
                generator: generateOpaqueGrayscaleRamp,
                premultiplyAlpha: false,
                format: gl.LUMINANCE_ALPHA,
                type: gl.UNSIGNED_BYTE,
                verifier: allChannelsIncreaseByNoMoreThan,
                threshold: 1,
                numOccurrences: 1,
                description: "LUMINANCE_ALPHA/UNSIGNED_BYTE should maintain full precision of data"
            });
        }
    }

    // Verify that setting the UNPACK_PREMULTIPLY_ALPHA_WEBGL pixel
    // store parameter and sending down a zero alpha causes the color
    // channels to go to zero.
    for (var dataMode = 0; dataMode < DataMode.NUM_MODES; ++dataMode) {
        for (var useTexSubImage2D = 0; useTexSubImage2D < 2; ++useTexSubImage2D) {
            testCases.push({
                dataMode: dataMode,
                useTexSubImage2D: !!useTexSubImage2D,
                width: 256,
                height: 1,
                generator: generateTransparentGrayscaleRamp,
                premultiplyAlpha: true,
                format: gl.RGBA,
                type: gl.UNSIGNED_BYTE,
                verifier: colorChannelsAreZero,
                description: "UNPACK_PREMULTIPLY_ALPHA_WEBGL with RGBA/UNSIGNED_BYTE"
            });
            testCases.push({
                dataMode: dataMode,
                useTexSubImage2D: !!useTexSubImage2D,
                width: 256,
                height: 1,
                generator: generateTransparentGrayscaleRamp,
                premultiplyAlpha: true,
                format: gl.RGBA,
                type: gl.UNSIGNED_SHORT_4_4_4_4,
                verifier: colorChannelsAreZero,
                description: "UNPACK_PREMULTIPLY_ALPHA_WEBGL with RGBA/UNSIGNED_SHORT_4_4_4_4"
            });
            testCases.push({
                dataMode: dataMode,
                useTexSubImage2D: !!useTexSubImage2D,
                width: 256,
                height: 1,
                generator: generateTransparentGrayscaleRamp,
                premultiplyAlpha: true,
                format: gl.RGBA,
                type: gl.UNSIGNED_SHORT_5_5_5_1,
                verifier: colorChannelsAreZero,
                description: "UNPACK_PREMULTIPLY_ALPHA_WEBGL with RGBA/UNSIGNED_SHORT_5_5_5_1"
            });
            // The following few tests are invalid for the raw data
            // mode because there is either no alpha channel or no
            // separate alpha channel.
            if (dataMode != DataMode.RAW_DATA) {
                testCases.push({
                    dataMode: dataMode,
                    useTexSubImage2D: !!useTexSubImage2D,
                    width: 256,
                    height: 1,
                    generator: generateTransparentGrayscaleRamp,
                    premultiplyAlpha: true,
                    format: gl.RGB,
                    type: gl.UNSIGNED_BYTE,
                    verifier: colorChannelsAreZero,
                    description: "UNPACK_PREMULTIPLY_ALPHA_WEBGL with RGB/UNSIGNED_BYTE"
                });
                testCases.push({
                    dataMode: dataMode,
                    useTexSubImage2D: !!useTexSubImage2D,
                    width: 256,
                    height: 1,
                    generator: generateTransparentGrayscaleRamp,
                    premultiplyAlpha: true,
                    format: gl.RGB,
                    type: gl.UNSIGNED_SHORT_5_6_5,
                    verifier: colorChannelsAreZero,
                    description: "UNPACK_PREMULTIPLY_ALPHA_WEBGL with RGB/UNSIGNED_SHORT_5_6_5"
                });
                testCases.push({
                    dataMode: dataMode,
                    useTexSubImage2D: !!useTexSubImage2D,
                    width: 256,
                    height: 1,
                    generator: generateTransparentGrayscaleRamp,
                    premultiplyAlpha: true,
                    format: gl.ALPHA,
                    type: gl.UNSIGNED_BYTE,
                    verifier: colorChannelsAreZero,
                    description: "UNPACK_PREMULTIPLY_ALPHA_WEBGL with ALPHA/UNSIGNED_BYTE"
                });
                testCases.push({
                    dataMode: dataMode,
                    useTexSubImage2D: !!useTexSubImage2D,
                    width: 256,
                    height: 1,
                    generator: generateTransparentGrayscaleRamp,
                    premultiplyAlpha: true,
                    format: gl.LUMINANCE,
                    type: gl.UNSIGNED_BYTE,
                    verifier: colorChannelsAreZero,
                    description: "UNPACK_PREMULTIPLY_ALPHA_WEBGL with LUMINANCE/UNSIGNED_BYTE"
                });
            }
            testCases.push({
                dataMode: dataMode,
                useTexSubImage2D: !!useTexSubImage2D,
                width: 256,
                height: 1,
                generator: generateTransparentGrayscaleRamp,
                premultiplyAlpha: true,
                format: gl.LUMINANCE_ALPHA,
                type: gl.UNSIGNED_BYTE,
                verifier: colorChannelsAreZero,
                description: "UNPACK_PREMULTIPLY_ALPHA_WEBGL with LUMINANCE_ALPHA/UNSIGNED_BYTE"
            });
        }
    }

    // Produce data for all testcases. Because we load images, some of
    // these may generate their data asynchronously.
    generateTestData();
}

function generateTestData()
{
    for (var i = 0; i < testCases.length; i++) {
        var testCase = testCases[i];
        var wrapper = null;
        switch (testCase.dataMode) {
        case DataMode.IMAGE:
            wrapper = new ImageWrapper(testCase.width, testCase.height);
            break;
        case DataMode.IMAGE_DATA:
            wrapper = new ImageDataWrapper(testCase.width, testCase.height);
            break;
        case DataMode.RAW_DATA:
            switch (testCase.type) {
            case gl.UNSIGNED_BYTE:
                switch (testCase.format) {
                case gl.RGBA:
                    wrapper = new RGBA8DataWrapper(testCase.width, testCase.height);
                    break;
                case gl.RGB:
                    wrapper = new RGB8DataWrapper(testCase.width, testCase.height);
                    break;
                case gl.ALPHA:
                    wrapper = new A8DataWrapper(testCase.width, testCase.height);
                    break;
                case gl.LUMINANCE:
                    wrapper = new L8DataWrapper(testCase.width, testCase.height);
                    break;
                case gl.LUMINANCE_ALPHA:
                    wrapper = new LA8DataWrapper(testCase.width, testCase.height);
                    break;
                }
                break;
            case gl.UNSIGNED_SHORT_4_4_4_4:
                wrapper = new RGBA4444DataWrapper(testCase.width, testCase.height);
                break;
            case gl.UNSIGNED_SHORT_5_5_5_1:
                wrapper = new RGBA5551DataWrapper(testCase.width, testCase.height);
                break;
            case gl.UNSIGNED_SHORT_5_6_5:
                wrapper = new RGB565DataWrapper(testCase.width, testCase.height);
                break;
            }
        }
        testCase.wrapper = wrapper;
        testCase.generator(wrapper);
        testCase.wrapper.generateData();
    }

    // See whether we need to run the tests, in case all of them
    // generated their results synchronously.
    maybeRunTests();
}

var ranTests = false;

function maybeRunTests()
{
    if (!ranTests)
        for (var i = 0; i < testCases.length; ++i)
            if (!testCases[i].wrapper || !testCases[i].wrapper.data)
                return;

    ranTests = true;

    for (var i = 0; i < testCases.length; ++i)
        runOneTest(testCases[i]);

    finishTest();
}

function testCaseToString(testCase)
{
    var mode;
    switch (testCase.dataMode) {
    case DataMode.IMAGE:
        mode = "Image";
        break;
    case DataMode.IMAGE_DATA:
        mode = "ImageData";
        break;
    case DataMode.RAW_DATA:
        mode = "raw data";
        break;
    }
    return (testCase.useTexSubImage2D ? "texSubImage2D" : "texImage2D") +
            " with " + mode +  " at " + testCase.width + "x" + testCase.height;
}

function runOneTest(testCase)
{
    debug("Testing " + testCaseToString(testCase));
    var data = testCase.wrapper.data;
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    var texture = gl.createTexture();
    // Bind the texture to texture unit 0.
    gl.bindTexture(gl.TEXTURE_2D, texture);
    // Set up texture parameters.
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    // Set up pixel store parameters.
    gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, testCase.premultiplyAlpha);
    // Upload the image into the texture.
    if (testCase.useTexSubImage2D) {
        // Initialize the texture to black first.
        gl.texImage2D(gl.TEXTURE_2D, 0, testCase.format, testCase.width, testCase.height, 0,
                      testCase.format, testCase.type, null);
    }
    switch (testCase.dataMode) {
    case DataMode.IMAGE:
    case DataMode.IMAGE_DATA:
        if (testCase.useTexSubImage2D)
            gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, testCase.format, testCase.type, data);
        else
            gl.texImage2D(gl.TEXTURE_2D, 0, testCase.format, testCase.format, testCase.type, data);
        break;
    case DataMode.RAW_DATA:
        if (testCase.useTexSubImage2D)
            gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, testCase.width, testCase.height, testCase.format, testCase.type, data);
        else
            gl.texImage2D(gl.TEXTURE_2D, 0, testCase.format, testCase.width, testCase.height, 0, testCase.format, testCase.type, data);
        break;
    }
    // Point the uniform sampler to texture unit 0.
    gl.uniform1i(textureLoc, 0);
    // Draw the triangles.
    gl.drawArrays(gl.TRIANGLES, 0, 6);
    // Clean up the texture.
    gl.deleteTexture(texture);

    // Read back the rendering results.
    var buf = new Uint8Array(testCase.width * testCase.height * 4);
    gl.readPixels(0, 0, testCase.width, testCase.height, gl.RGBA, gl.UNSIGNED_BYTE, buf);
    // Run the verification routine.
    if (testCase.verifier(buf, testCase.threshold, testCase.numOccurrences))
        testPassed(testCase.description);
    else
        testFailed(testCase.description);
}

//----------------------------------------------------------------------
// Wrappers for programmatic construction of Image, ImageData and raw texture data
//

function ImageWrapper(width, height)
{
    this.pngBuilder_ = new PNGlib(width, height, 256);
}

ImageWrapper.prototype.getWidth = function() {
    return this.pngBuilder_.width;
};

ImageWrapper.prototype.getHeight = function() {
    return this.pngBuilder_.height;
};

ImageWrapper.prototype.setPixel = function(x, y, r, g, b, a) {
    this.pngBuilder_.buffer[this.pngBuilder_.index(x, y)] = this.pngBuilder_.color(r, g, b, a);
};

// Generates data into "data" property, possibly asynchronously.
ImageWrapper.prototype.generateData = function() {
    var that = this;
    var url = "data:image/png;base64," + this.pngBuilder_.getBase64();
    var img = wtu.makeImage(url, function() {
      that.data = img;
      maybeRunTests();
    });
};

function ImageDataWrapper(width, height)
{
    if (!ImageDataWrapper.tempCanvas) {
        ImageDataWrapper.tempCanvas = document.createElement("canvas");
    }
    this.imageData_ = ImageDataWrapper.tempCanvas.getContext("2d").createImageData(width, height);
}

ImageDataWrapper.tempCanvas = null;

ImageDataWrapper.prototype.getWidth = function() {
    return this.imageData_.width;
};

ImageDataWrapper.prototype.getHeight = function() {
    return this.imageData_.height;
};

ImageDataWrapper.prototype.setPixel = function(x, y, r, g, b, a) {
    var index = 4 * (this.imageData_.width * y + x);
    this.imageData_.data[index] = r;
    this.imageData_.data[index + 1] = g;
    this.imageData_.data[index + 2] = b;
    this.imageData_.data[index + 3] = a;
};

ImageDataWrapper.prototype.generateData = function() {
    this.data = this.imageData_;
    maybeRunTests();
};

function TextureDataWrapper(width, height)
{
    this.width_ = width;
    this.height_ = height;
}

TextureDataWrapper.prototype.getWidth = function() {
    return this.width_;
};

TextureDataWrapper.prototype.getHeight = function() {
    return this.height_;
};

TextureDataWrapper.prototype.generateData = function() {
    this.data = this.data_;
    maybeRunTests();
};

function RGBA8DataWrapper(width, height)
{
    TextureDataWrapper.call(this, width, height);
    this.data_ = new Uint8Array(4 * width * height);
}

RGBA8DataWrapper.prototype = new TextureDataWrapper;

RGBA8DataWrapper.prototype.setPixel = function(x, y, r, g, b, a) {
    var index = 4 * (this.width_ * y + x);
    this.data_[index] = r;
    this.data_[index + 1] = g;
    this.data_[index + 2] = b;
    this.data_[index + 3] = a;
};

function RGBA5551DataWrapper(width, height)
{
    TextureDataWrapper.call(this, width, height);
    this.data_ = new Uint16Array(width * height);
}

RGBA5551DataWrapper.prototype = new TextureDataWrapper;

RGBA5551DataWrapper.prototype.setPixel = function(x, y, r, g, b, a) {
    var value = (((r & 0xF8) << 8)
                 | ((g & 0xF8) << 3)
                 | ((b & 0xF8) >> 2)
                 | (a >> 7));
    this.data_[this.width_ * y + x] = value;
};

function RGBA4444DataWrapper(width, height)
{
    TextureDataWrapper.call(this, width, height);
    this.data_ = new Uint16Array(width * height);
}

RGBA4444DataWrapper.prototype = new TextureDataWrapper;

RGBA4444DataWrapper.prototype.setPixel = function(x, y, r, g, b, a) {
    var value = (((r & 0xF0) << 8)
                 | ((g & 0xF0) << 4)
                 | (b & 0xF0)
                 | (a >> 4));
    this.data_[this.width_ * y + x] = value;
};

function RGB8DataWrapper(width, height)
{
    TextureDataWrapper.call(this, width, height);
    this.data_ = new Uint8Array(3 * width * height);
}

RGB8DataWrapper.prototype = new TextureDataWrapper;

RGB8DataWrapper.prototype.setPixel = function(x, y, r, g, b, a) {
    var index = 3 * (this.width_ * y + x);
    this.data_[index] = r;
    this.data_[index + 1] = g;
    this.data_[index + 2] = b;
};

function RGB565DataWrapper(width, height)
{
    TextureDataWrapper.call(this, width, height);
    this.data_ = new Uint16Array(width * height);
}

RGB565DataWrapper.prototype = new TextureDataWrapper;

RGB565DataWrapper.prototype.setPixel = function(x, y, r, g, b, a) {
    var value = (((r & 0xF8) << 8)
                 | ((g & 0xFC) << 3)
                 | ((b & 0xF8) >> 3));
    this.data_[this.width_ * y + x] = value;
};

function A8DataWrapper(width, height)
{
    TextureDataWrapper.call(this, width, height);
    this.data_ = new Uint8Array(width * height);
}

A8DataWrapper.prototype = new TextureDataWrapper;

A8DataWrapper.prototype.setPixel = function(x, y, r, g, b, a) {
    this.data_[this.width_ * y + x] = a;
};

function L8DataWrapper(width, height)
{
    TextureDataWrapper.call(this, width, height);
    this.data_ = new Uint8Array(width * height);
}

L8DataWrapper.prototype = new TextureDataWrapper;

L8DataWrapper.prototype.setPixel = function(x, y, r, g, b, a) {
    this.data_[this.width_ * y + x] = r;
};

function LA8DataWrapper(width, height)
{
    TextureDataWrapper.call(this, width, height);
    this.data_ = new Uint8Array(2 * width * height);
}

LA8DataWrapper.prototype = new TextureDataWrapper;

LA8DataWrapper.prototype.setPixel = function(x, y, r, g, b, a) {
    var index = 2 * (this.width_ * y + x);
    this.data_[index] = r;
    this.data_[index + 1] = a;
};

//----------------------------------------------------------------------
// Color ramp generation functions
//

function generateOpaqueGrayscaleRamp(wrapper)
{
    var width = wrapper.getWidth();
    var height = wrapper.getHeight();
    for (var x = 0; x < width; ++x) {
        var value = Math.round(255.0 * x / width);
        for (var y = 0; y < height; ++y)
            wrapper.setPixel(x, y, value, value, value, 255);
    }
}

function generateTranslucentGrayscaleRamp(wrapper)
{
    var width = wrapper.getWidth();
    var height = wrapper.getHeight();
    for (var x = 0; x < width; ++x) {
        var value = Math.round(255.0 * x / width);
        for (var y = 0; y < height; ++y)
            wrapper.setPixel(x, y, value, value, value, value);
    }
}

function generateTransparentGrayscaleRamp(wrapper)
{
    var width = wrapper.getWidth();
    var height = wrapper.getHeight();
    for (var x = 0; x < width; ++x) {
        var value = Math.round(255.0 * x / width);
        for (var y = 0; y < height; ++y)
            wrapper.setPixel(x, y, value, value, value, 0);
    }
}

//----------------------------------------------------------------------
// Verification routines
//

function allChannelsIncreaseByNoMoreThan(array, threshold, numOccurrences) {
    var numFound = 0;
    for (var i = 4; i < array.length; i += 4)
        for (var j = 0; j < 4; j++)
            if (array[i + j] - array[i + j - 4] > threshold)
                ++numFound;

    return numFound < numOccurrences;
}

function alphaChannelIncreasesByNoMoreThan(array, threshold, numOccurrences) {
    var numFound = 0;
    for (var i = 7; i < array.length; i += 4)
        if (array[i] - array[i - 4] > threshold)
            ++numFound;

    return numFound < numOccurrences;
}

function allChannelsIncreaseByAtLeast(array, threshold, numOccurrences) {
    var numFound = 0;
    for (var i = 4; i < array.length; i += 4)
        for (var j = 0; j < 4; ++j)
            if (array[i + j] - array[i + j - 4] > threshold)
                ++numFound;

    return numFound > numOccurrences;
}

function colorChannelsAreZero(array, threshold, numOccurrences) {
    var passed = true;
    var numFailures = 0;

    for (var i = 4; i < array.length; i += 4)
        for (var j = 0; j < 3; ++j)
            if (array[i + j] != 0) {
                passed = false;
                if (++numFailures <= 5)
                    debug("  array[" + (i + j) + "] should have been 0, was " + array[i + j]);
            }

    return passed;
}

</script>
</head>
<body onload="init()">
<canvas id="example" width="256" height="1"></canvas>
<div id="description"></div>
<div id="console"></div>
</body>
</html>
