"use strict";
var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        if (typeof b !== "function" && b !== null)
            throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
var utils_1 = require("../../../utils");
var node_image_vert_glsl_1 = __importDefault(require("../shaders/node.image.vert.glsl.js"));
var node_image_frag_glsl_1 = __importDefault(require("../shaders/node.image.frag.glsl.js"));
var node_1 = require("./common/node");
// maximum size of single texture in atlas
var MAX_TEXTURE_SIZE = 192;
// maximum width of atlas texture (limited by browser)
// low setting of 3072 works on phones & tablets
var MAX_CANVAS_WIDTH = 3072;
var UNIFORMS = ["u_sizeRatio", "u_pixelRatio", "u_matrix", "u_atlas"];
/**
 * To share the texture between the program instances of the graph and the
 * hovered nodes (to prevent some flickering, mostly), this program must be
 * "built" for each sigma instance:
 */
function getNodeImageProgram(drawLabel, drawHover) {
    var _a;
    /**
     * These attributes are shared between all instances of this exact class,
     * returned by this call to getNodeProgramImage:
     */
    var rebindTextureFns = [];
    var images = {};
    var textureImage;
    var hasReceivedImages = false;
    var pendingImagesFrameID = undefined;
    // next write position in texture
    var writePositionX = 0;
    var writePositionY = 0;
    // height of current row
    var writeRowHeight = 0;
    /**
     * Helper to load an image:
     */
    function loadImage(imageSource) {
        if (images[imageSource])
            return;
        var image = new Image();
        image.addEventListener("load", function () {
            images[imageSource] = {
                status: "pending",
                image: image,
            };
            if (typeof pendingImagesFrameID !== "number") {
                pendingImagesFrameID = requestAnimationFrame(function () { return finalizePendingImages(); });
            }
        });
        image.addEventListener("error", function () {
            images[imageSource] = { status: "error" };
        });
        images[imageSource] = { status: "loading" };
        // Load image:
        image.setAttribute("crossOrigin", "");
        image.src = imageSource;
    }
    /**
     * Helper that takes all pending images and adds them into the texture:
     */
    function finalizePendingImages() {
        pendingImagesFrameID = undefined;
        var pendingImages = [];
        // List all pending images:
        for (var id in images) {
            var state = images[id];
            if (state.status === "pending") {
                pendingImages.push({
                    id: id,
                    image: state.image,
                    size: Math.min(state.image.width, state.image.height) || 1,
                });
            }
        }
        // Add images to texture:
        var canvas = document.createElement("canvas");
        var ctx = canvas.getContext("2d", { willReadFrequently: true });
        // limit canvas size to avoid browser and platform limits
        var totalWidth = hasReceivedImages ? textureImage.width : 0;
        var totalHeight = hasReceivedImages ? textureImage.height : 0;
        // initialize image drawing offsets with current write position
        var xOffset = writePositionX;
        var yOffset = writePositionY;
        /**
         * Draws a (full or partial) row of images into the atlas texture
         * @param pendingImages
         */
        var drawRow = function (pendingImages) {
            // update canvas size before drawing
            if (canvas.width !== totalWidth || canvas.height !== totalHeight) {
                canvas.width = Math.min(MAX_CANVAS_WIDTH, totalWidth);
                canvas.height = totalHeight;
                // draw previous texture into resized canvas
                if (hasReceivedImages) {
                    ctx.putImageData(textureImage, 0, 0);
                }
            }
            pendingImages.forEach(function (_b) {
                var id = _b.id, image = _b.image, size = _b.size;
                var imageSizeInTexture = Math.min(MAX_TEXTURE_SIZE, size);
                // Crop image, to only keep the biggest square, centered:
                var dx = 0, dy = 0;
                if ((image.width || 0) > (image.height || 0)) {
                    dx = (image.width - image.height) / 2;
                }
                else {
                    dy = (image.height - image.width) / 2;
                }
                ctx.drawImage(image, dx, dy, size, size, xOffset, yOffset, imageSizeInTexture, imageSizeInTexture);
                // Update image state:
                images[id] = {
                    status: "ready",
                    x: xOffset,
                    y: yOffset,
                    width: imageSizeInTexture,
                    height: imageSizeInTexture,
                };
                xOffset += imageSizeInTexture;
            });
            hasReceivedImages = true;
            textureImage = ctx.getImageData(0, 0, canvas.width, canvas.height);
        };
        var rowImages = [];
        pendingImages.forEach(function (image) {
            var size = image.size;
            var imageSizeInTexture = Math.min(size, MAX_TEXTURE_SIZE);
            if (writePositionX + imageSizeInTexture > MAX_CANVAS_WIDTH) {
                // existing row is full: flush row and continue on next line
                if (rowImages.length > 0) {
                    totalWidth = Math.max(writePositionX, totalWidth);
                    totalHeight = Math.max(writePositionY + writeRowHeight, totalHeight);
                    drawRow(rowImages);
                    rowImages = [];
                    writeRowHeight = 0;
                }
                writePositionX = 0;
                writePositionY = totalHeight;
                xOffset = 0;
                yOffset = totalHeight;
            }
            // add image to row
            rowImages.push(image);
            // advance write position and update maximum row height
            writePositionX += imageSizeInTexture;
            writeRowHeight = Math.max(writeRowHeight, imageSizeInTexture);
        });
        // flush pending images in row - keep write position (and drawing cursor)
        totalWidth = Math.max(writePositionX, totalWidth);
        totalHeight = Math.max(writePositionY + writeRowHeight, totalHeight);
        drawRow(rowImages);
        rowImages = [];
        rebindTextureFns.forEach(function (fn) { return fn(); });
    }
    var UNSIGNED_BYTE = WebGLRenderingContext.UNSIGNED_BYTE, FLOAT = WebGLRenderingContext.FLOAT;
    return _a = /** @class */ (function (_super) {
            __extends(NodeImageProgram, _super);
            function NodeImageProgram(gl, pickingBuffer, renderer) {
                var _this = _super.call(this, gl, pickingBuffer, renderer) || this;
                rebindTextureFns.push(function () {
                    if (_this && _this.bindTexture) {
                        _this.bindTexture();
                        if (_this.latestRenderParams)
                            _this.render(_this.latestRenderParams);
                    }
                    if (renderer && renderer.refresh)
                        renderer.refresh();
                });
                textureImage = new ImageData(1, 1);
                _this.texture = gl.createTexture();
                gl.bindTexture(gl.TEXTURE_2D, _this.texture);
                gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 0]));
                return _this;
            }
            NodeImageProgram.prototype.getDefinition = function () {
                return {
                    VERTICES: 1,
                    VERTEX_SHADER_SOURCE: node_image_vert_glsl_1.default,
                    FRAGMENT_SHADER_SOURCE: node_image_frag_glsl_1.default,
                    METHOD: WebGLRenderingContext.POINTS,
                    UNIFORMS: UNIFORMS,
                    ATTRIBUTES: [
                        { name: "a_position", size: 2, type: FLOAT },
                        { name: "a_size", size: 1, type: FLOAT },
                        { name: "a_color", size: 4, type: UNSIGNED_BYTE, normalized: true },
                        { name: "a_id", size: 4, type: UNSIGNED_BYTE, normalized: true },
                        { name: "a_texture", size: 4, type: FLOAT },
                    ],
                };
            };
            NodeImageProgram.prototype.bindTexture = function () {
                var gl = this.normalProgram.gl;
                gl.bindTexture(gl.TEXTURE_2D, this.texture);
                gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureImage);
                gl.generateMipmap(gl.TEXTURE_2D);
            };
            NodeImageProgram.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);
                }
                // Rebind texture (since it's been just unbound by picking):
                var gl = this.normalProgram.gl;
                gl.bindTexture(gl.TEXTURE_2D, this.texture);
                // Draw normal program:
                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);
            };
            NodeImageProgram.prototype.processVisibleItem = function (nodeIndex, startIndex, data) {
                var array = this.array;
                var imageSource = data.image;
                var imageState = imageSource && images[imageSource];
                if (typeof imageSource === "string" && !imageState)
                    loadImage(imageSource);
                array[startIndex++] = data.x;
                array[startIndex++] = data.y;
                array[startIndex++] = data.size;
                array[startIndex++] = (0, utils_1.floatColor)(data.color);
                array[startIndex++] = nodeIndex;
                // Reference texture:
                if (imageState && imageState.status === "ready") {
                    var width = textureImage.width, height = textureImage.height;
                    array[startIndex++] = imageState.x / width;
                    array[startIndex++] = imageState.y / height;
                    array[startIndex++] = imageState.width / width;
                    array[startIndex++] = imageState.height / height;
                }
                else {
                    array[startIndex++] = 0;
                    array[startIndex++] = 0;
                    array[startIndex++] = 0;
                    array[startIndex++] = 0;
                }
            };
            NodeImageProgram.prototype.setUniforms = function (params, _b) {
                var gl = _b.gl, uniformLocations = _b.uniformLocations, isPicking = _b.isPicking;
                var sizeRatio = params.sizeRatio, downSizedSizeRatio = params.downSizedSizeRatio, pixelRatio = params.pixelRatio, matrix = params.matrix;
                this.latestRenderParams = params;
                var u_sizeRatio = uniformLocations.u_sizeRatio, u_pixelRatio = uniformLocations.u_pixelRatio, u_matrix = uniformLocations.u_matrix, u_atlas = uniformLocations.u_atlas;
                gl.uniform1f(u_pixelRatio, pixelRatio);
                gl.uniform1f(u_sizeRatio, isPicking ? downSizedSizeRatio : sizeRatio);
                gl.uniformMatrix3fv(u_matrix, false, matrix);
                gl.uniform1i(u_atlas, 0);
            };
            return NodeImageProgram;
        }(node_1.NodeProgram)),
        _a.drawLabel = drawLabel,
        _a.drawHover = drawHover,
        _a;
}
exports.default = getNodeImageProgram;
