<!--
Copyright (c) 2021 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>
<div id="description"></div>
<div id="console"></div>
<script>
"use strict";

const wtu = WebGLTestUtils;
description();

const gl = wtu.create3DContext();
gl.canvas.width = gl.canvas.height = 1;

function makeTexImage(format, unpackFormat, unpackType, data) {
    data = data || null;

    const tex = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, tex);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl[format], 2, 2, 0,
                gl[unpackFormat], gl[unpackType], null);
    return tex;
}

const DUMMY_COLOR = makeTexImage('RGBA', 'RGBA', 'UNSIGNED_BYTE');

function makeProgram(gl, vsSrc, fsSrc) {
  function makeShader(prog, type, src) {
    const shader = gl.createShader(gl[type]);
    gl.shaderSource(shader, src.trim());
    gl.compileShader(shader);
    gl.attachShader(prog, shader);
    gl.deleteShader(shader);
  };

  const prog = gl.createProgram();
  makeShader(prog, 'VERTEX_SHADER', vsSrc);
  makeShader(prog, 'FRAGMENT_SHADER', fsSrc);
  gl.linkProgram(prog);

  if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
    throw 'Program linking failed' + fsSrc;
  }
  return prog;
}

const TEX_FILTER_PROG_T = (version, samplerT) => makeProgram(gl, `\
    ${version}
    void main() {
        gl_PointSize = 1.0;
        gl_Position = vec4(0.5,0.5,0,1);
    }`,`\
    ${version}
    precision mediump float;
    uniform ${samplerT} u_tex0;
    #if __VERSION__ == 300
    out vec4 o_FragColor;
    #else
    #define o_FragColor gl_FragColor
    #define texture texture2D
    #endif
    void main() {
        o_FragColor = vec4(texture(u_tex0, vec2(0.8)));
    }`);
const TEX_FILTER_PROG_BY_TYPEISH = {
    'float': TEX_FILTER_PROG_T('', 'sampler2D'),
};
if (wtu.isWebGL2(gl)) {
    TEX_FILTER_PROG_BY_TYPEISH['int'] =
        TEX_FILTER_PROG_T('#version 300 es', 'highp isampler2D');
    TEX_FILTER_PROG_BY_TYPEISH['uint'] =
        TEX_FILTER_PROG_T('#version 300 es', 'highp usampler2D');
}

function runPixelProgram(gl, prog) {
    gl.useProgram(prog);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.POINTS, 0, 1);
    const bytes = new Uint8Array(4);
    gl.readPixels(0,0,1,1,gl.RGBA, gl.UNSIGNED_BYTE, bytes);
    return [].map.call(bytes, x => x/255.0);
}

// GLES 2.0.25 p63
const FORMAT_INFO_WEBGL1 = {
    RGBA8  : { filter: true, render: true     , unpack: ['RGBA', 'UNSIGNED_BYTE'] },
    RGB8   : { filter: true, render: undefined, unpack: ['RGB', 'UNSIGNED_BYTE'] },
    RGBA4  : { filter: true, render: undefined, unpack: ['RGBA', 'UNSIGNED_SHORT_4_4_4_4'] },
    RGB5_A1: { filter: true, render: undefined, unpack: ['RGBA', 'UNSIGNED_SHORT_5_5_5_1'] },
    RGB565 : { filter: true, render: undefined, unpack: ['RGB', 'UNSIGNED_SHORT_5_6_5'] },
    LA8    : { filter: true, render: false    , unpack: ['LUMINANCE_ALPHA', 'UNSIGNED_BYTE'] },
    L8     : { filter: true, render: false    , unpack: ['LUMINANCE', 'UNSIGNED_BYTE'] },
    A8     : { filter: true, render: false    , unpack: ['ALPHA', 'UNSIGNED_BYTE'] },
};

// GLES 3.0.6 p130-132
const FORMAT_INFO_WEBGL2 = {
    R8            : { render: true , filter: true , unpack: ['RED', 'UNSIGNED_BYTE'] },
    R8_SNORM      : { render: false, filter: true , unpack: ['RED', 'BYTE'] },
    RG8           : { render: true , filter: true , unpack: ['RG', 'UNSIGNED_BYTE'] },
    RG8_SNORM     : { render: false, filter: true , unpack: ['RG', 'BYTE'] },
    RGB8          : { render: true , filter: true , unpack: ['RGB', 'UNSIGNED_BYTE'] },
    RGB8_SNORM    : { render: false, filter: true , unpack: ['RGB', 'BYTE'] },
    RGB565        : { render: true , filter: true , unpack: ['RGB', 'UNSIGNED_SHORT_5_6_5'] },
    RGBA4         : { render: true , filter: true , unpack: ['RGBA', 'UNSIGNED_SHORT_4_4_4_4'] },
    RGB5_A1       : { render: true , filter: true , unpack: ['RGBA', 'UNSIGNED_SHORT_5_5_5_1'] },
    RGBA8         : { render: true , filter: true , unpack: ['RGBA', 'UNSIGNED_BYTE'] },
    RGBA8_SNORM   : { render: false, filter: true , unpack: ['RGBA', 'BYTE'] },
    RGB10_A2      : { render: true , filter: true , unpack: ['RGBA', 'UNSIGNED_INT_10_10_10_2'] },
    RGB10_A2UI    : { render: true , filter: false, unpack: ['RGBA', 'UNSIGNED_INT_10_10_10_2'] },
    SRGB8         : { render: false, filter: true , unpack: ['RGB', 'UNSIGNED_BYTE'] },
    SRGB8_ALPHA8  : { render: true , filter: true , unpack: ['RGBA', 'UNSIGNED_BYTE'] },
    R16F          : { render: false, filter: true , unpack: ['RED', 'FLOAT'] },
    RG16F         : { render: false, filter: true , unpack: ['RG', 'FLOAT'] },
    RGB16F        : { render: false, filter: true , unpack: ['RGB', 'FLOAT'] },
    RGBA16F       : { render: false, filter: true , unpack: ['RGBA', 'FLOAT'] },
    R32F          : { render: false, filter: false, unpack: ['RED', 'FLOAT'] },
    RG32F         : { render: false, filter: false, unpack: ['RG', 'FLOAT'] },
    RGB32F        : { render: false, filter: false, unpack: ['RGB', 'FLOAT'] },
    RGBA32F       : { render: false, filter: false, unpack: ['RGBA', 'FLOAT'] },
    R11F_G11F_B10F: { render: false, filter: true , unpack: ['RGB', 'FLOAT'] },
    RGB9_E5       : { render: false, filter: true , unpack: ['RGB', 'FLOAT'] },
    R8I           : { render: true , filter: false, unpack: ['RED', 'BYTE'] },
    R8UI          : { render: true , filter: false, unpack: ['RED', 'UNSIGNED_BYTE'] },
    R16I          : { render: true , filter: false, unpack: ['RED', 'BYTE'] },
    R16UI         : { render: true , filter: false, unpack: ['RED', 'UNSIGNED_BYTE'] },
    R32I          : { render: true , filter: false, unpack: ['RED', 'BYTE'] },
    R32UI         : { render: true , filter: false, unpack: ['RED', 'UNSIGNED_BYTE'] },
    RG8I          : { render: true , filter: false, unpack: ['RG', 'BYTE'] },
    RG8UI         : { render: true , filter: false, unpack: ['RG', 'UNSIGNED_BYTE'] },
    RG16I         : { render: true , filter: false, unpack: ['RG', 'SHORT'] },
    RG16UI        : { render: true , filter: false, unpack: ['RG', 'UNSIGNED_SHORT'] },
    RG32I         : { render: true , filter: false, unpack: ['RG', 'INT'] },
    RG32UI        : { render: true , filter: false, unpack: ['RG', 'UNSIGNED_INT'] },
    RGB8I         : { render: false, filter: false, unpack: ['RGB', 'BYTE'] },
    RGB8UI        : { render: false, filter: false, unpack: ['RGB', 'UNSIGNED_BYTE'] },
    RGB16I        : { render: false, filter: false, unpack: ['RGB', 'SHORT'] },
    RGB16UI       : { render: false, filter: false, unpack: ['RGB', 'UNSIGNED_SHORT'] },
    RGB32I        : { render: false, filter: false, unpack: ['RGB', 'INT'] },
    RGB32UI       : { render: false, filter: false, unpack: ['RGB', 'UNSIGNED_INT'] },
    RGBA8I        : { render: true , filter: false, unpack: ['RGBA', 'BYTE'] },
    RGBA8UI       : { render: true , filter: false, unpack: ['RGBA', 'UNSIGNED_BYTE'] },
    RGBA16I       : { render: true , filter: false, unpack: ['RGBA', 'SHORT'] },
    RGBA16UI      : { render: true , filter: false, unpack: ['RGBA', 'UNSIGNED_SHORT'] },
    RGBA32I       : { render: true , filter: false, unpack: ['RGBA', 'INT'] },
    RGBA32UI      : { render: true , filter: false, unpack: ['RGBA', 'UNSIGNED_INT'] },

    DEPTH_COMPONENT16:  { render: 'DEPTH_ATTACHMENT', filter: false },
    DEPTH_COMPONENT24:  { render: 'DEPTH_ATTACHMENT', filter: false },
    DEPTH_COMPONENT32F: { render: 'DEPTH_ATTACHMENT', filter: false },
    DEPTH24_STENCIL8:   { render: 'DEPTH_STENCIL_ATTACHMENT', filter: false },
    DEPTH32F_STENCIL8:  { render: 'DEPTH_STENCIL_ATTACHMENT', filter: false },
};

const ONE_BY_TYPE = {
    'BYTE': (1<<7)-1,
    'UNSIGNED_BYTE': (1<<8)-1,
    'SHORT': (1<<15)-1,
    'UNSIGNED_SHORT': (1<<16)-1,
    'INT': (1<<31)-1,
    'UNSIGNED_INT': Math.pow(2,32)-1,
    'FLOAT': 1,
};

const ABV_BY_TYPE = {
    'BYTE': Int8Array,
    'UNSIGNED_BYTE': Uint8Array,
    'SHORT': Int16Array,
    'UNSIGNED_SHORT': Uint16Array,
    'INT': Int32Array,
    'UNSIGNED_INT': Uint32Array,
    'FLOAT': Float32Array,
};

function pushBitsUnorm(prev, bitCount, floatVal) {
    let ret = prev << bitCount;
    ret |= floatVal * ((1 << bitCount)-1);
    return ret;
}

const CHANNELS_BY_FORMAT = {
    'RED': 1,
    'LUMINANCE': 1,
    'ALPHA': 1,
    'LUMINANCE_ALPHA': 2,
    'RG': 2,
    'RGB': 3,
    'RGBA': 4,
};

function throwv(val) {
    throw val;
}

function pixelDataForUnpack(format, type, floatVal) {
    switch (type) {
        case 'UNSIGNED_SHORT_5_6_5': {
            let bits = 0;
            bits = pushBitsUnorm(bits, 5, floatVal);
            bits = pushBitsUnorm(bits, 6, floatVal);
            bits = pushBitsUnorm(bits, 5, floatVal);
            return new Uint16Array([bits]);
        }
        case 'UNSIGNED_SHORT_4_4_4_4': {
            let bits = 0;
            bits = pushBitsUnorm(bits, 4, floatVal);
            bits = pushBitsUnorm(bits, 4, floatVal);
            bits = pushBitsUnorm(bits, 4, floatVal);
            bits = pushBitsUnorm(bits, 4, floatVal);
            return new Uint16Array([bits]);
        }
        case 'UNSIGNED_SHORT_5_5_5_1': {
            let bits = 0;
            bits = pushBitsUnorm(bits, 5, floatVal);
            bits = pushBitsUnorm(bits, 5, floatVal);
            bits = pushBitsUnorm(bits, 5, floatVal);
            bits = pushBitsUnorm(bits, 1, floatVal); // Ok, silly for 1 bit here.
            return new Uint16Array([bits]);
        }
        case 'UNSIGNED_INT_10_10_10_2': {
            let bits = 0;
            bits = pushBitsUnorm(bits, 10, floatVal);
            bits = pushBitsUnorm(bits, 10, floatVal);
            bits = pushBitsUnorm(bits, 10, floatVal);
            bits = pushBitsUnorm(bits, 2, floatVal); // 2 bits isn't much more useful
            return new Uint32Array([bits]);
        }
    }

    const channels = CHANNELS_BY_FORMAT[format] || throwv(format);
    const one = ONE_BY_TYPE[type] || throwv('240', type);
    const abvType = ABV_BY_TYPE[type] || throwv('241', type);

    const val = floatVal * one;
    const arr = [];
    for (const i of range(channels)) {
        arr.push(val);
    }
    return new abvType(arr);
}

function expect(name, was, expected) {
    let text = `${name} was ${was}`;
    const cond = was == expected;
    if (!cond) {
        text += `, but expected ${expected}`;
    }
    expectTrue(cond, text);
}

function toTypeish(sizedFormat) {
    if (sizedFormat.endsWith('UI')) {
        return 'int';
    } else if (sizedFormat.endsWith('I')) {
        return 'uint';
    }
    return 'float';
}

call(async () => {
    gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);

    let formatList = FORMAT_INFO_WEBGL1;
    if (wtu.isWebGL2(gl)) {
        formatList = FORMAT_INFO_WEBGL2;
    }
    for (const [sizedFormat, info] of Object.entries(formatList)) {
        await wtu.dispatchPromise();
        debug(``);
        debug(`${sizedFormat}: ${JSON.stringify(info)}`);

        const typeish = toTypeish(sizedFormat);

        //  |---|---|
        //  | 0 | 1 |
        //  |---|---|
        // 0| 0 | 0 |
        //  |---|---|
        //    0
        const tex = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, tex);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

        if (gl.texStorage2D) {
            gl.texStorage2D(gl.TEXTURE_2D, 1, gl[sizedFormat], 2, 2);
        } else {
            gl.texImage2D(gl.TEXTURE_2D, 0, gl[info.unpack[0]],
                2, 2, 0, gl[info.unpack[0]], gl[info.unpack[1]], null);
        }

        if (info.unpack) {
            const one = pixelDataForUnpack(...info.unpack, 1.0);
            const data = new one.constructor(one.length*4);
            data.set(one, one.length*3);
            gl.texSubImage2D(gl.TEXTURE_2D, 0, 0,0,
                2,2, gl[info.unpack[0]], gl[info.unpack[1]], data);
        } else {
            info.render || throwv(`${sizedFormat} without unpack or render`);
        }

        // -
        // color-renderable test

        {
            const fb = gl.createFramebuffer();
            gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
            let attach = info.render || true;
            const isColor = (attach === true);
            if (isColor) {
                attach = 'COLOR_ATTACHMENT0';
            } else {
                gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
                                        gl.TEXTURE_2D, DUMMY_COLOR, 0);
            }
            gl.framebufferTexture2D(gl.FRAMEBUFFER, gl[attach],
                                    gl.TEXTURE_2D, tex, 0);
            const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
            const wasRenderable = (status == gl.FRAMEBUFFER_COMPLETE);
            if (info.render === undefined) {
                debug(`Non-normative: color-renderable was ${wasRenderable}`);
            } else {
                expect('color-renderable', wasRenderable, !!info.render);
            }
            if (wasRenderable) {
                gl.clearColor(0,0,0,0);
                gl.clearDepth(0);
                gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
                gl.enable(gl.SCISSOR_TEST);
                gl.scissor(1,1,1,1);
                gl.clearColor(1,1,1,1);
                gl.clearDepth(1);
                gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
                gl.disable(gl.SCISSOR_TEST);
            }
            gl.deleteFramebuffer(fb);
            if (!wasRenderable && !info.unpack) {
                testFailed('No unpack provided and !wasRenderable, skipping filtering subtest...');
                continue;
            }
        }

        // -
        // filterable test

        const prog = TEX_FILTER_PROG_BY_TYPEISH[typeish];
        gl.bindTexture(gl.TEXTURE_2D, tex);
        gl.clearColor(0,0,0,0);
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);
        gl.viewport(0,0,1,1);
        const v = runPixelProgram(gl, prog);
        if (sizedFormat != 'A8') {
            v[3] = 0; // Incomplete no-alpha formats put 1 in alpha.
        }
        const wasFilterable = v.some(x => !!x);
        expect('filterable', wasFilterable, info.filter);
    }

    finishTest();
});

</script>
</body>
</html>
