<!--
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/webgl-test-utils.js"></script>
</head>
<body>
<canvas id="example" width="4" height="4"></canvas>
<div id="description"></div>
<div id="console"></div>
<script>
"use strict"

var wtu = WebGLTestUtils;
var initialColor = [1, 2, 3, 4];
var expectedColor = [[249, 102, 0, 255],
                     [2, 200, 102, 255],
                     [134, 87, 234, 255],
                     [99, 5, 76, 255]];

function calculatePaddingBytes(bytesPerPixel, packAlignment, width)
{
    var padding = 0;
    switch (packAlignment) {
    case 1:
    case 2:
    case 4:
    case 8:
        padding = (bytesPerPixel * width) % packAlignment;
        if (padding > 0)
            padding = packAlignment - padding;
        return padding;
    default:
        testFailed("should not reach here");
        return;
    }
}

function paintWebGLCanvas(gl)
{
    var program = wtu.setupTexturedQuad(gl);
    gl.disable(gl.DEPTH_TEST);
    gl.disable(gl.BLEND);

    var data = new Uint8Array(4 * 4);
    for (var ii = 0; ii < 4; ++ii) {
        data[ii * 4] = expectedColor[ii][0];
        data[ii * 4 + 1] = expectedColor[ii][1];
        data[ii * 4 + 2] = expectedColor[ii][2];
        data[ii * 4 + 3] = expectedColor[ii][3];
    }

    var tex = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, tex);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 4, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);

    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

    var loc = gl.getUniformLocation(program, "tex");
    gl.uniform1i(loc, 0);

    wtu.clearAndDrawUnitQuad(gl);
}

function samePixel(array, index, refPixel, row, pixelTag)
{
    for (var ii = 0; ii < refPixel.length; ++ii) {
      if (array[index] == refPixel[ii][0] &&
          array[index + 1] == refPixel[ii][1] &&
          array[index + 2] == refPixel[ii][2] &&
          array[index + 3] == refPixel[ii][3]) {
          return true;
      }
    }
    var refPixelText = "";
    for (var ii = 0; ii < refPixel.length; ++ii) {
        if (ii > 0)
            refPixelText += " or ";
        refPixelText += "[" + refPixel[ii] + "]";
    }
    testFailed(pixelTag + " pixel of row " + row + ": expected " + refPixelText + ", got [" +
        [array[index], array[index + 1], array[index + 2], array[index + 3]] + "]");
    return false;
}

function runTestIteration(xoffset, yoffset, width, height, packParams, usePixelPackBuffer, packParamsValid)
{
    if (!("alignment" in packParams))
        packParams.alignment = 4;
    if (!("rowLength" in packParams))
        packParams.rowLength = 0;
    if (!("skipPixels" in packParams))
        packParams.skipPixels = 0;
    if (!("skipRows" in packParams))
        packParams.skipRows = 0;
    debug("Testing xoffset = " + xoffset + ", yoffset " + yoffset +
          ", width = " + width + ", height = " + height +
          ", PACK_ALIGNMENT = " + packParams.alignment + ", PACK_ROW_LENGTH = " + packParams.rowLength +
          ", PACK_SKIP_PIXELS = " + packParams.skipPixels + " , PACK_SKIP_ROWS = " + packParams.skipRows);
    gl.pixelStorei(gl.PACK_ALIGNMENT, packParams.alignment);
    gl.pixelStorei(gl.PACK_ROW_LENGTH, packParams.rowLength);
    gl.pixelStorei(gl.PACK_SKIP_PIXELS, packParams.skipPixels);
    gl.pixelStorei(gl.PACK_SKIP_ROWS, packParams.skipRows);

    var actualWidth = packParams.rowLength > 0 ? packParams.rowLength : width;

    var bytesPerPixel = 4; // see readPixels' parameters below, the format is gl.RGBA, type is gl.UNSIGNED_BYTE
    var padding = calculatePaddingBytes(bytesPerPixel, packParams.alignment, actualWidth);
    var bytesPerRow = actualWidth * bytesPerPixel + padding;

    var size = bytesPerRow * (height - 1) + bytesPerPixel * width;
    var skipSize = packParams.skipPixels * bytesPerPixel + packParams.skipRows * bytesPerRow;
    var array = new Uint8Array(skipSize + size);
    for (var ii = 0; ii < skipSize + size; ++ii) {
        array[ii] = initialColor[ii % bytesPerPixel];
    }
    var arrayWrongSize = null;
    if (size > 0)
        arrayWrongSize = new Uint8Array(skipSize + size - 1);
    if (usePixelPackBuffer) {
        var offset = 0;

        var buffer = gl.createBuffer();
        gl.bindBuffer(gl.PIXEL_PACK_BUFFER, buffer);
        if (size > 0) {
            gl.bufferData(gl.PIXEL_PACK_BUFFER, arrayWrongSize, gl.STATIC_DRAW);
            gl.readPixels(xoffset, yoffset, width, height, gl.RGBA, gl.UNSIGNED_BYTE, offset);
            wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "buffer too small");
        }
        gl.bufferData(gl.PIXEL_PACK_BUFFER, array, gl.STATIC_DRAW);
        gl.readPixels(xoffset, yoffset, width, height, gl.RGBA, gl.UNSIGNED_BYTE, offset);
    } else {
        if (size > 0) {
            gl.readPixels(xoffset, yoffset, width, height, gl.RGBA, gl.UNSIGNED_BYTE, arrayWrongSize);
            wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "buffer too small");
        }
        gl.readPixels(xoffset, yoffset, width, height, gl.RGBA, gl.UNSIGNED_BYTE, array);
    }
    if (packParamsValid) {
        wtu.glErrorShouldBe(gl, gl.NO_ERROR, "readPixels should succeed");
    } else {
        wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Invalid pack params combination");
        return;
    }

    if (size == 0)
        return;

    if (usePixelPackBuffer) {
        array = new Uint8Array(skipSize + size);
        gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, array);
    }

    // Check skipped bytes are unchanged.
    for (var ii = 0; ii < skipSize; ++ii) {
        if (array[ii] != initialColor[ii % bytesPerPixel]) {
            testFailed("skipped bytes changed at index " + ii + ": expected " +
                       initialColor[ii % bytesPerPixel] + " got " + array[ii]);
            break;
        }
    }
    // Check the first and last pixels of each row.
    var canvasWidth = 4;
    var canvasHeight = 4;
    for (var row = 0; row < height; ++row) {
        var refColor;
        var yIndex = yoffset + row;
        var xIndex;

        // First pixel
        var pos = skipSize + bytesPerRow * row;
        xIndex = xoffset;
        if (xIndex < 0 || xIndex >= canvasWidth || yIndex < 0 || yIndex >= canvasHeight) {
            if (row > 0 && usePixelPackBuffer && packParams.rowLength > 0 && packParams.rowLength < width)
                refColor = [initialColor, expectedColor[yIndex - 1]];
            else
                refColor = [initialColor];
        } else {
            refColor = [expectedColor[yIndex]];
        }
        samePixel(array, pos, refColor, row, "first");

        // Last pixel
        var xSpan;
        if (row + 1 == height || packParams.rowLength > width)
            xSpan = width;
        else
            xSpan = actualWidth;
        xIndex = xoffset + xSpan - 1;
        pos += (xSpan - 1) * bytesPerPixel;
        if (xIndex < 0 || xIndex >= canvasWidth || yIndex < 0 || yIndex >= canvasHeight) {
            if (row > 0 && usePixelPackBuffer && packParams.rowLength > 0 && packParams.rowLength < width)
                refColor = [initialColor, expectedColor[yIndex - 1]];
            else
                refColor = [initialColor];
        } else {
            refColor = [expectedColor[yIndex]];
        }
        samePixel(array, pos, refColor, row, "last");

        // Check padding bytes are unchanged and bytes beyond rowLength set correctly.
        pos += bytesPerPixel;
        if (row + 1 < height) {
            // Beyond bytes filled for PACK_ROW_LENGTH, the row could have extra bytes due to
            // padding. These extra bytes could be either filled with pixel data if
            // PACK_ROW_LENGTH is set to be less than width, or they could be left unchanged
            // if they are beyond |width| pixels.
            if (packParams.rowLength > 0 && packParams.rowLength < width) {
                var trailingBytes = Math.min((width - packParams.rowLength) * bytesPerPixel,
                                             bytesPerRow - packParams.rowLength * bytesPerPixel);
                for (var ii = 0; ii < trailingBytes; ++ii) {
                    if (array[pos + ii] != refColor[0][ii % bytesPerPixel]) {
                        testFailed("Trailing byte " + ii + " after rowLength of row " + row + " : expected " +
                                   refColor[0][ii % bytesPerPixel] + ", got " + array[pos + ii]);
                        break;
                    }
                }
                pos += trailingBytes;
            }
            var paddingBytes = skipSize + bytesPerRow * (row + 1) - pos;
            for (var ii = 0; ii < paddingBytes; ++ii) {
                if (array[pos + ii] != initialColor[ii % bytesPerPixel]) {
                    testFailed("Padding byte " + ii + " of row " + row + " changed: expected " +
                               initialColor[ii % bytesPerPixel] + ", got " + array[pos + ii]);
                    break;
                }
            }
        }
    }
}

function testPackParameters(usePixelPackBuffer)
{
    debug("");
    var destText = usePixelPackBuffer ? "PIXEL_PACK buffer" : "array buffer";
    debug("Verify that reading pixels to " + destText + " works fine with various pack alignments.");
    runTestIteration(0, 0, 1, 3, {alignment:1}, usePixelPackBuffer, true);
    runTestIteration(0, 0, 1, 3, {alignment:2}, usePixelPackBuffer, true);
    runTestIteration(0, 0, 1, 3, {alignment:4}, usePixelPackBuffer, true);
    runTestIteration(0, 0, 1, 3, {alignment:8}, usePixelPackBuffer, true);
    runTestIteration(0, 0, 2, 3, {alignment:4}, usePixelPackBuffer, true);
    runTestIteration(0, 0, 2, 3, {alignment:8}, usePixelPackBuffer, true);
    runTestIteration(0, 0, 3, 3, {alignment:4}, usePixelPackBuffer, true);
    runTestIteration(0, 0, 3, 3, {alignment:8}, usePixelPackBuffer, true);
    runTestIteration(0, 0, 0, 0, {alignment:1}, usePixelPackBuffer, true);
    runTestIteration(0, 0, 1, 3, {alignment:4}, usePixelPackBuffer, true);
    runTestIteration(0, 0, 1, 3, {alignment:8}, usePixelPackBuffer, true);

    debug("");
    debug("Verify that reading pixels to " + destText + " is disallowed when PACK_ROW_LENGTH < width.");
    runTestIteration(0, 0, 3, 3, {alignment:8, rowLength:2}, usePixelPackBuffer, false);
    runTestIteration(-1, 0, 3, 3, {alignment:8, rowLength:2}, usePixelPackBuffer, false);
    runTestIteration(0, -1, 3, 3, {alignment:8, rowLength:2}, usePixelPackBuffer, false);
    runTestIteration(-1, -1, 3, 3, {alignment:8, rowLength:2}, usePixelPackBuffer, false);
    runTestIteration(-5, 0, 3, 3, {alignment:8, rowLength:2}, usePixelPackBuffer, false);
    runTestIteration(0, -5, 3, 3, {alignment:8, rowLength:2}, usePixelPackBuffer, false);
    runTestIteration(2, 0, 3, 3, {alignment:8, rowLength:2}, usePixelPackBuffer, false);
    runTestIteration(0, 2, 3, 3, {alignment:8, rowLength:2}, usePixelPackBuffer, false);
    runTestIteration(2, 2, 3, 3, {alignment:8, rowLength:2}, usePixelPackBuffer, false);
    runTestIteration(5, 0, 3, 3, {alignment:8, rowLength:2}, usePixelPackBuffer, false);
    runTestIteration(0, 5, 3, 3, {alignment:8, rowLength:2}, usePixelPackBuffer, false);
    runTestIteration(0, 0, 3, 3, {alignment:8, rowLength:1}, usePixelPackBuffer, false);

    debug("");
    debug("Verify that reading pixels to " + destText + " works fine with PACK_ROW_LENGTH == width.");
    runTestIteration(0, 0, 3, 3, {alignment:8, rowLength:3}, usePixelPackBuffer, true);
    runTestIteration(-1, 0, 3, 3, {alignment:8, rowLength:3}, usePixelPackBuffer, true);
    runTestIteration(0, -1, 3, 3, {alignment:8, rowLength:3}, usePixelPackBuffer, true);
    runTestIteration(-1, -1, 3, 3, {alignment:8, rowLength:3}, usePixelPackBuffer, true);
    runTestIteration(-5, 0, 3, 3, {alignment:8, rowLength:3}, usePixelPackBuffer, true);
    runTestIteration(0, -5, 3, 3, {alignment:8, rowLength:3}, usePixelPackBuffer, true);
    runTestIteration(2, 0, 3, 3, {alignment:8, rowLength:3}, usePixelPackBuffer, true);
    runTestIteration(0, 2, 3, 3, {alignment:8, rowLength:3}, usePixelPackBuffer, true);
    runTestIteration(2, 2, 3, 3, {alignment:8, rowLength:3}, usePixelPackBuffer, true);
    runTestIteration(5, 0, 3, 3, {alignment:8, rowLength:3}, usePixelPackBuffer, true);
    runTestIteration(0, 5, 3, 3, {alignment:8, rowLength:3}, usePixelPackBuffer, true);

    debug("");
    debug("Verify that reading pixels to " + destText + " works fine with PACK_ROW_LENGTH > width and with no padding");
    runTestIteration(0, 0, 3, 3, {alignment:8, rowLength:4}, usePixelPackBuffer, true);
    runTestIteration(-1, 0, 3, 3, {alignment:8, rowLength:4}, usePixelPackBuffer, true);
    runTestIteration(0, -1, 3, 3, {alignment:8, rowLength:4}, usePixelPackBuffer, true);
    runTestIteration(-1, -1, 3, 3, {alignment:8, rowLength:4}, usePixelPackBuffer, true);
    runTestIteration(-5, 0, 3, 3, {alignment:8, rowLength:4}, usePixelPackBuffer, true);
    runTestIteration(0, -5, 3, 3, {alignment:8, rowLength:4}, usePixelPackBuffer, true);
    runTestIteration(2, 0, 3, 3, {alignment:8, rowLength:4}, usePixelPackBuffer, true);
    runTestIteration(0, 2, 3, 3, {alignment:8, rowLength:4}, usePixelPackBuffer, true);
    runTestIteration(2, 2, 3, 3, {alignment:8, rowLength:4}, usePixelPackBuffer, true);
    runTestIteration(5, 0, 3, 3, {alignment:8, rowLength:4}, usePixelPackBuffer, true);
    runTestIteration(0, 5, 3, 3, {alignment:8, rowLength:4}, usePixelPackBuffer, true);

    debug("");
    debug("Verify that reading pixels to " + destText + " works fine with PACK_ROW_LENGTH > width and with padding");
    runTestIteration(0, 0, 3, 3, {alignment:8, rowLength:5}, usePixelPackBuffer, true);
    runTestIteration(-1, 0, 3, 3, {alignment:8, rowLength:5}, usePixelPackBuffer, true);
    runTestIteration(0, -1, 3, 3, {alignment:8, rowLength:5}, usePixelPackBuffer, true);
    runTestIteration(-1, -1, 3, 3, {alignment:8, rowLength:5}, usePixelPackBuffer, true);
    runTestIteration(-5, 0, 3, 3, {alignment:8, rowLength:5}, usePixelPackBuffer, true);
    runTestIteration(0, -5, 3, 3, {alignment:8, rowLength:5}, usePixelPackBuffer, true);
    runTestIteration(2, 0, 3, 3, {alignment:8, rowLength:5}, usePixelPackBuffer, true);
    runTestIteration(0, 2, 3, 3, {alignment:8, rowLength:5}, usePixelPackBuffer, true);
    runTestIteration(2, 2, 3, 3, {alignment:8, rowLength:5}, usePixelPackBuffer, true);
    runTestIteration(5, 0, 3, 3, {alignment:8, rowLength:5}, usePixelPackBuffer, true);
    runTestIteration(0, 5, 3, 3, {alignment:8, rowLength:5}, usePixelPackBuffer, true);

    debug("");
    debug("Verify that reading pixels to " + destText + " works fine with pack skip parameters.");
    runTestIteration(0, 0, 3, 3, {alignment:8, skipPixels:2}, usePixelPackBuffer, false);
    runTestIteration(0, 0, 3, 3, {alignment:8, skipPixels:1, skipRows:3}, usePixelPackBuffer, false);
    runTestIteration(0, 0, 3, 3, {alignment:8, skipRows:3}, usePixelPackBuffer, true);
    runTestIteration(0, 0, 2, 3, {alignment:8, skipPixels:2}, usePixelPackBuffer, false);
    runTestIteration(0, 0, 2, 3, {alignment:8, skipPixels:1, skipRows:3}, usePixelPackBuffer, false);
    runTestIteration(0, 0, 2, 3, {alignment:8, skipRows:3}, usePixelPackBuffer, true);
    runTestIteration(0, 0, 2, 3, {skipPixels:1, rowLength:4}, usePixelPackBuffer, true);
}

debug("");
debug("Canvas.getContext");

var canvas = document.getElementById("example");
var gl = wtu.create3DContext(canvas, undefined, 2);

if (!gl) {
  testFailed("context does not exist");
} else {
  testPassed("context exists");

  debug("");
  description('ReadPixels into array buffer');
  paintWebGLCanvas(gl);
  var usePixelPackBuffer = false;
  testPackParameters(usePixelPackBuffer);
  usePixelPackBuffer = true;
  testPackParameters(usePixelPackBuffer);
}

debug("");
var successfullyParsed = true;
</script>
<script src="../../js/js-test-post.js"></script>
</body>
</html>
