"use strict";
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Program = exports.AbstractProgram = void 0;
var utils_1 = require("../../shaders/utils");
var PICKING_PREFIX = "#define PICKING_MODE\n";
var SIZE_FACTOR_PER_ATTRIBUTE_TYPE = (_a = {},
    _a[WebGL2RenderingContext.BOOL] = 1,
    _a[WebGL2RenderingContext.BYTE] = 1,
    _a[WebGL2RenderingContext.UNSIGNED_BYTE] = 1,
    _a[WebGL2RenderingContext.SHORT] = 2,
    _a[WebGL2RenderingContext.UNSIGNED_SHORT] = 2,
    _a[WebGL2RenderingContext.INT] = 4,
    _a[WebGL2RenderingContext.UNSIGNED_INT] = 4,
    _a[WebGL2RenderingContext.FLOAT] = 4,
    _a);
function getAttributeItemsCount(attr) {
    return attr.normalized ? 1 : attr.size;
}
function getAttributesItemsCount(attrs) {
    var res = 0;
    attrs.forEach(function (attr) { return (res += getAttributeItemsCount(attr)); });
    return res;
}
var AbstractProgram = /** @class */ (function () {
    // eslint-disable-next-line @typescript-eslint/no-empty-function
    function AbstractProgram(_gl, _pickGl, _renderer) {
    }
    return AbstractProgram;
}());
exports.AbstractProgram = AbstractProgram;
var Program = /** @class */ (function () {
    function Program(gl, pickingBuffer, renderer) {
        this.array = new Float32Array();
        this.constantArray = new Float32Array();
        this.capacity = 0;
        this.verticesCount = 0;
        // Reading and caching program definition
        var def = this.getDefinition();
        this.VERTICES = def.VERTICES;
        this.VERTEX_SHADER_SOURCE = def.VERTEX_SHADER_SOURCE;
        this.FRAGMENT_SHADER_SOURCE = def.FRAGMENT_SHADER_SOURCE;
        this.UNIFORMS = def.UNIFORMS;
        this.ATTRIBUTES = def.ATTRIBUTES;
        this.METHOD = def.METHOD;
        this.CONSTANT_ATTRIBUTES = "CONSTANT_ATTRIBUTES" in def ? def.CONSTANT_ATTRIBUTES : [];
        this.CONSTANT_DATA = "CONSTANT_DATA" in def ? def.CONSTANT_DATA : [];
        this.isInstanced = "CONSTANT_ATTRIBUTES" in def;
        // Computing stride
        this.ATTRIBUTES_ITEMS_COUNT = getAttributesItemsCount(this.ATTRIBUTES);
        this.STRIDE = this.VERTICES * this.ATTRIBUTES_ITEMS_COUNT;
        // Members
        this.renderer = renderer;
        this.normalProgram = this.getProgramInfo("normal", gl, def.VERTEX_SHADER_SOURCE, def.FRAGMENT_SHADER_SOURCE, null);
        this.pickProgram = pickingBuffer
            ? this.getProgramInfo("pick", gl, PICKING_PREFIX + def.VERTEX_SHADER_SOURCE, PICKING_PREFIX + def.FRAGMENT_SHADER_SOURCE, pickingBuffer)
            : null;
        // For instanced programs:
        if (this.isInstanced) {
            var constantAttributesItemsCount = getAttributesItemsCount(this.CONSTANT_ATTRIBUTES);
            if (this.CONSTANT_DATA.length !== this.VERTICES)
                throw new Error("Program: error while getting constant data (expected ".concat(this.VERTICES, " items, received ").concat(this.CONSTANT_DATA.length, " instead)"));
            this.constantArray = new Float32Array(this.CONSTANT_DATA.length * constantAttributesItemsCount);
            for (var i = 0; i < this.CONSTANT_DATA.length; i++) {
                var vector = this.CONSTANT_DATA[i];
                if (vector.length !== constantAttributesItemsCount)
                    throw new Error("Program: error while getting constant data (one vector has ".concat(vector.length, " items instead of ").concat(constantAttributesItemsCount, ")"));
                for (var j = 0; j < vector.length; j++)
                    this.constantArray[i * constantAttributesItemsCount + j] = vector[j];
            }
            this.STRIDE = this.ATTRIBUTES_ITEMS_COUNT;
        }
    }
    Program.prototype.getProgramInfo = function (name, gl, vertexShaderSource, fragmentShaderSource, frameBuffer) {
        var def = this.getDefinition();
        // WebGL buffers
        var buffer = gl.createBuffer();
        if (buffer === null)
            throw new Error("Program: error while creating the WebGL buffer.");
        // Shaders and program
        var vertexShader = (0, utils_1.loadVertexShader)(gl, vertexShaderSource);
        var fragmentShader = (0, utils_1.loadFragmentShader)(gl, fragmentShaderSource);
        var program = (0, utils_1.loadProgram)(gl, [vertexShader, fragmentShader]);
        // Initializing locations
        var uniformLocations = {};
        def.UNIFORMS.forEach(function (uniformName) {
            var location = gl.getUniformLocation(program, uniformName);
            if (location)
                uniformLocations[uniformName] = location;
        });
        var attributeLocations = {};
        def.ATTRIBUTES.forEach(function (attr) {
            attributeLocations[attr.name] = gl.getAttribLocation(program, attr.name);
        });
        // For instanced programs:
        var constantBuffer;
        if ("CONSTANT_ATTRIBUTES" in def) {
            def.CONSTANT_ATTRIBUTES.forEach(function (attr) {
                attributeLocations[attr.name] = gl.getAttribLocation(program, attr.name);
            });
            constantBuffer = gl.createBuffer();
            if (constantBuffer === null)
                throw new Error("Program: error while creating the WebGL constant buffer.");
        }
        return {
            name: name,
            program: program,
            gl: gl,
            frameBuffer: frameBuffer,
            buffer: buffer,
            constantBuffer: constantBuffer || {},
            uniformLocations: uniformLocations,
            attributeLocations: attributeLocations,
            isPicking: name === "pick",
        };
    };
    Program.prototype.bindProgram = function (program) {
        var _this = this;
        var offset = 0;
        var gl = program.gl, buffer = program.buffer;
        if (!this.isInstanced) {
            gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
            offset = 0;
            this.ATTRIBUTES.forEach(function (attr) { return (offset += _this.bindAttribute(attr, program, offset)); });
            gl.bufferData(gl.ARRAY_BUFFER, this.array, gl.DYNAMIC_DRAW);
        }
        else {
            // Handle constant data (things that remain unchanged for all items):
            gl.bindBuffer(gl.ARRAY_BUFFER, program.constantBuffer);
            offset = 0;
            this.CONSTANT_ATTRIBUTES.forEach(function (attr) { return (offset += _this.bindAttribute(attr, program, offset, false)); });
            gl.bufferData(gl.ARRAY_BUFFER, this.constantArray, gl.STATIC_DRAW);
            // Handle "instance specific" data (things that vary for each item):
            gl.bindBuffer(gl.ARRAY_BUFFER, program.buffer);
            offset = 0;
            this.ATTRIBUTES.forEach(function (attr) { return (offset += _this.bindAttribute(attr, program, offset, true)); });
            gl.bufferData(gl.ARRAY_BUFFER, this.array, gl.DYNAMIC_DRAW);
        }
        gl.bindBuffer(gl.ARRAY_BUFFER, null);
    };
    Program.prototype.unbindProgram = function (program) {
        var _this = this;
        if (!this.isInstanced) {
            this.ATTRIBUTES.forEach(function (attr) { return _this.unbindAttribute(attr, program); });
        }
        else {
            this.CONSTANT_ATTRIBUTES.forEach(function (attr) { return _this.unbindAttribute(attr, program, false); });
            this.ATTRIBUTES.forEach(function (attr) { return _this.unbindAttribute(attr, program, true); });
        }
    };
    Program.prototype.bindAttribute = function (attr, program, offset, setDivisor) {
        var sizeFactor = SIZE_FACTOR_PER_ATTRIBUTE_TYPE[attr.type];
        if (typeof sizeFactor !== "number")
            throw new Error("Program.bind: yet unsupported attribute type \"".concat(attr.type, "\""));
        var location = program.attributeLocations[attr.name];
        var gl = program.gl;
        if (location !== -1) {
            gl.enableVertexAttribArray(location);
            var stride = !this.isInstanced
                ? this.ATTRIBUTES_ITEMS_COUNT * Float32Array.BYTES_PER_ELEMENT
                : (setDivisor ? this.ATTRIBUTES_ITEMS_COUNT : getAttributesItemsCount(this.CONSTANT_ATTRIBUTES)) *
                    Float32Array.BYTES_PER_ELEMENT;
            gl.vertexAttribPointer(location, attr.size, attr.type, attr.normalized || false, stride, offset);
            if (this.isInstanced && setDivisor) {
                if (gl instanceof WebGL2RenderingContext) {
                    gl.vertexAttribDivisor(location, 1);
                }
                else {
                    var ext = gl.getExtension("ANGLE_instanced_arrays");
                    if (ext)
                        ext.vertexAttribDivisorANGLE(location, 1);
                }
            }
        }
        return attr.size * sizeFactor;
    };
    Program.prototype.unbindAttribute = function (attr, program, unsetDivisor) {
        var location = program.attributeLocations[attr.name];
        var gl = program.gl;
        if (location !== -1) {
            gl.disableVertexAttribArray(location);
            if (this.isInstanced && unsetDivisor) {
                if (gl instanceof WebGL2RenderingContext) {
                    gl.vertexAttribDivisor(location, 0);
                }
                else {
                    var ext = gl.getExtension("ANGLE_instanced_arrays");
                    if (ext)
                        ext.vertexAttribDivisorANGLE(location, 0);
                }
            }
        }
    };
    Program.prototype.reallocate = function (capacity) {
        // If desired capacity has not changed we do nothing
        // NOTE: it's possible here to implement more subtle reallocation schemes
        // when the number of rendered items increase or decrease
        if (capacity === this.capacity)
            return;
        this.capacity = capacity;
        this.verticesCount = this.VERTICES * capacity;
        this.array = new Float32Array(!this.isInstanced
            ? this.verticesCount * this.ATTRIBUTES_ITEMS_COUNT
            : this.capacity * this.ATTRIBUTES_ITEMS_COUNT);
    };
    Program.prototype.hasNothingToRender = function () {
        return this.verticesCount === 0;
    };
    Program.prototype.renderProgram = function (params, programInfo) {
        var gl = programInfo.gl, program = programInfo.program, isPicking = programInfo.isPicking;
        if (!isPicking)
            gl.enable(gl.BLEND);
        else
            gl.disable(gl.BLEND);
        gl.useProgram(program);
        this.setUniforms(params, programInfo);
        this.drawWebGL(this.METHOD, programInfo);
    };
    Program.prototype.render = function (params) {
        if (this.hasNothingToRender())
            return;
        if (this.pickProgram) {
            this.pickProgram.gl.viewport(0, 0, (params.width * params.pixelRatio) / params.downSizingRatio, (params.height * params.pixelRatio) / params.downSizingRatio);
            this.bindProgram(this.pickProgram);
            this.renderProgram(params, this.pickProgram);
            this.unbindProgram(this.pickProgram);
        }
        this.normalProgram.gl.viewport(0, 0, params.width * params.pixelRatio, params.height * params.pixelRatio);
        this.bindProgram(this.normalProgram);
        this.renderProgram(params, this.normalProgram);
        this.unbindProgram(this.normalProgram);
    };
    Program.prototype.drawWebGL = function (method, _a) {
        var gl = _a.gl, frameBuffer = _a.frameBuffer;
        gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer);
        if (!this.isInstanced) {
            gl.drawArrays(method, 0, this.verticesCount);
        }
        else {
            if (gl instanceof WebGL2RenderingContext) {
                gl.drawArraysInstanced(method, 0, this.VERTICES, this.capacity);
            }
            else {
                var ext = gl.getExtension("ANGLE_instanced_arrays");
                if (ext)
                    ext.drawArraysInstancedANGLE(method, 0, this.VERTICES, this.capacity);
            }
        }
    };
    return Program;
}());
exports.Program = Program;
