import { assert } from "../../../../core/assert.js";
import { Base64 } from "../../../../core/binary/base64/Base64.js";
import {
    compute_typed_array_constructor_from_data_type
} from "../../../../core/binary/type/DataType2TypedArrayConstructorMapping.js";
import { array_buffer_hash } from "../../../../core/collection/array/typed/array_buffer_hash.js";
import {
    compute_binary_data_type_from_typed_array
} from "../../../../core/collection/array/typed/compute_binary_data_type_from_typed_array.js";
import { is_typed_array_equals } from "../../../../core/collection/array/typed/is_typed_array_equals.js";
import { isTypedArray } from "../../../../core/collection/array/typed/isTypedArray.js";
import {
    typedArrayConstructorByInstance
} from "../../../../core/collection/array/typed/typedArrayConstructorByInstance.js";
import { clamp } from "../../../../core/math/clamp.js";
import { lerp } from "../../../../core/math/lerp.js";
import { max2 } from "../../../../core/math/max2.js";
import { min2 } from "../../../../core/math/min2.js";
import { interpolate_bicubic } from "../../../../core/math/spline/interpolate_bicubic.js";
import { computeHashFloat } from "../../../../core/primitives/numbers/computeHashFloat.js";

/**
 * Data Texture class, providing an abstraction around 2d numerical arrays, mostly for sampling purposes
 * Inspired by OpenGL's glsl sampler2d API
 * @class
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class Sampler2D {
    /**
     *
     * @param {ArrayLike<number>|number[]|Uint8ClampedArray|Uint8Array|Uint16Array|Uint32Array|Int8Array|Int16Array|Int32Array|Float32Array|Float64Array} data
     * @param {number} [itemSize]
     * @param {number} [width]
     * @param {number} [height]
     * @constructor
     */
    constructor(data = [], itemSize = 1, width = 0, height = 0) {
        if (!Number.isInteger(itemSize) || itemSize < 0) {
            throw new Error(`itemSize must be a non-negative integer, instead was ${itemSize}`);
        }
        if (!Number.isInteger(width) || width < 0) {
            throw new Error(`width must be a non-negative integer, instead was ${width}`);
        }
        if (!Number.isInteger(height) || width < 0) {
            throw new Error(`height must be a non-negative integer, instead was ${height}`);
        }

        if (data === undefined) {
            throw new Error("data was undefined");
        }

        if (data.length < width * height * itemSize) {
            throw new Error(`Buffer underflow, data.length(=${data.length}) is too small. Expected at least ${width * height * itemSize}`);
        }

        /**
         *
         * @type {number}
         */
        this.width = width;

        /**
         *
         * @type {number}
         */
        this.height = height;

        /**
         * Number of channels
         * @type {number}
         */
        this.itemSize = itemSize;

        /**
         *
         * @type {number[]|Uint8ClampedArray|Uint8Array|Uint16Array|Uint32Array|Int8Array|Int16Array|Int32Array|Float32Array|Float64Array}
         */
        this.data = data;

        /**
         * Used to tracking changes
         * @type {number}
         */
        this.version = 0;
    }

    /**
     *
     * @param {number} u
     * @param {number} v
     * @param {number[]} result
     */
    sampleCatmullRomUV(u, v, result) {
        const itemSize = this.itemSize;

        for (let i = 0; i < itemSize; i++) {
            result[i] = this.sampleChannelCatmullRomUV(u, v, i);
        }
    }

    /**
     *
     * @param {number} u
     * @param {number} v
     * @param {number} channel
     * @returns {number}
     */
    sampleChannelCatmullRomUV(u, v, channel) {
        const x = u * this.width - 0.5;
        const y = v * this.height - 0.5;

        return this.sampleChannelCatmullRom(x, y, channel);
    }

    /**
     *
     * @see https://gist.github.com/TheRealMJP/c83b8c0f46b63f3a88a5986f4fa982b1
     * @param {number} x
     * @param {number} y
     * @param {number} channel
     * @returns {number}
     */
    sampleChannelCatmullRom(x, y, channel) {
        // We're going to sample a 4x4 grid of texels surrounding the target UV coordinate. We'll do this by rounding
        // down the sample location to get the exact center of our "starting" texel. The starting texel will be at
        // location [1, 1] in the grid, where [0, 0] is the top-left corner.
        const texPos1_x = Math.floor(x);
        const texPos1_y = Math.floor(y);

        // Compute the fractional offset from our starting texel to our original sample location, which we'll
        // feed into the Catmull-Rom spline function to get our filter weights.
        const f_x = x - texPos1_x;
        const f_y = y - texPos1_y;

        // Compute the Catmull-Rom weights using the fractional offset that we calculated earlier.
        // These equations are pre-expanded based on our knowledge of where the texels will be located,
        // which lets us avoid having to evaluate a piece-wise function.
        const w0_x = f_x * (-0.5 + f_x * (1.0 - 0.5 * f_x));
        const w0_y = f_y * (-0.5 + f_y * (1.0 - 0.5 * f_y));

        const w1_x = 1.0 + f_x * f_x * (-2.5 + 1.5 * f_x);
        const w1_y = 1.0 + f_y * f_y * (-2.5 + 1.5 * f_y);

        const w2_x = f_x * (0.5 + f_x * (2.0 - 1.5 * f_x));
        const w2_y = f_y * (0.5 + f_y * (2.0 - 1.5 * f_y));

        const w3_x = f_x * f_x * (-0.5 + 0.5 * f_x);
        const w3_y = f_y * f_y * (-0.5 + 0.5 * f_y);

        // Work out weighting factors and sampling offsets that will let us use bilinear filtering to
        // simultaneously evaluate the middle 2 samples from the 4x4 grid.
        const w12_x = w1_x + w2_x;
        const w12_y = w1_y + w2_y;
        const offset12_x = w2_x / w12_x;
        const offset12_y = w2_y / w12_y;

        // Compute the final coordinates we'll use for sampling the texture
        const texPos0_x = texPos1_x - 1;
        const texPos0_y = texPos1_y - 1;
        const texPos3_x = texPos1_x + 2;
        const texPos3_y = texPos1_y + 2;
        const texPos12_x = texPos1_x + offset12_x;
        const texPos12_y = texPos1_y + offset12_y;

        let result = 0.0;

        result += this.sampleChannelBilinear(texPos0_x, texPos0_y, channel) * w0_x * w0_y;
        result += this.sampleChannelBilinear(texPos12_x, texPos0_y, channel) * w12_x * w0_y;
        result += this.sampleChannelBilinear(texPos3_x, texPos0_y, channel) * w3_x * w0_y;

        result += this.sampleChannelBilinear(texPos0_x, texPos12_y, channel) * w0_x * w12_y;
        result += this.sampleChannelBilinear(texPos12_x, texPos12_y, channel) * w12_x * w12_y;
        result += this.sampleChannelBilinear(texPos3_x, texPos12_y, channel) * w3_x * w12_y;

        result += this.sampleChannelBilinear(texPos0_x, texPos3_y, channel) * w0_x * w3_y;
        result += this.sampleChannelBilinear(texPos12_x, texPos3_y, channel) * w12_x * w3_y;
        result += this.sampleChannelBilinear(texPos3_x, texPos3_y, channel) * w3_x * w3_y;

        return result;
    }

    /**
     *
     * @param {number} u
     * @param {number} v
     * @param {number[]} result
     */
    sampleBicubicUV(u, v, result) {
        const itemSize = this.itemSize;

        for (let i = 0; i < itemSize; i++) {
            result[i] = this.sampleChannelBicubicUV(u, v, i);
        }
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @param {number[]|Float32Array|Float64Array} result
     * @param {number} result_offset
     */
    sampleBicubic(x, y, result, result_offset) {

        const itemSize = this.itemSize;

        for (let i = 0; i < itemSize; i++) {
            result[i + result_offset] = this.sampleChannelBicubic(x, y, i);

        }
    }

    /**
     *
     * @param {number} u
     * @param {number} v
     * @param {number} channel
     * @returns {number}
     */
    sampleChannelBicubicUV(u, v, channel) {
        const x = u * (this.width);
        const y = v * (this.height);

        return this.sampleChannelBicubic(x - 0.5, y - 0.5, channel);
    }

    /**
     *
     * Bicubic-filtered sampling, note values can be negative due to the nature of the cubic curve
     * @param {number} x
     * @param {number} y
     * @param {number} channel
     * @returns {number}
     */
    sampleChannelBicubic(x, y, channel) {

        const itemSize = this.itemSize;

        const width = this.width;
        const height = this.height;

        const data = this.data;

        const rowSize = width * itemSize;

        const x_max = width - 1;
        const y_max = height - 1;

        const clamped_x = clamp(x, 0, x_max)
        const clamped_y = clamp(y, 0, y_max)

        // fractional texel position (from the nearest low texel)
        const x1 = clamped_x | 0;
        const y1 = clamped_y | 0;

        const xd = clamped_x - x1;
        const yd = clamped_y - y1;

        const x0 = max2(0, x1 - 1);
        const y0 = max2(0, y1 - 1);

        const x2 = min2(x_max, x1 + 1);
        const y2 = min2(y_max, y1 + 1);

        const x3 = min2(x_max, x2 + 1);
        const y3 = min2(y_max, y2 + 1);

        // compute row offsets
        const row0 = y0 * rowSize;
        const row1 = y1 * rowSize;
        const row2 = y2 * rowSize;
        const row3 = y3 * rowSize;

        const row0_address = row0 + channel;
        const row1_address = row1 + channel;
        const row2_address = row2 + channel;
        const row3_address = row3 + channel;

        const col0_offset = x0 * itemSize;
        const col1_offset = x1 * itemSize;
        const col2_offset = x2 * itemSize;
        const col3_offset = x3 * itemSize;

        // read samples
        const vi0 = data[row0_address + col0_offset];
        const vi1 = data[row0_address + col1_offset];
        const vi2 = data[row0_address + col2_offset];
        const vi3 = data[row0_address + col3_offset];

        const vj0 = data[row1_address + col0_offset];
        const vj1 = data[row1_address + col1_offset];
        const vj2 = data[row1_address + col2_offset];
        const vj3 = data[row1_address + col3_offset];

        const vk0 = data[row2_address + col0_offset];
        const vk1 = data[row2_address + col1_offset];
        const vk2 = data[row2_address + col2_offset];
        const vk3 = data[row2_address + col3_offset];

        const vl0 = data[row3_address + col0_offset];
        const vl1 = data[row3_address + col1_offset];
        const vl2 = data[row3_address + col2_offset];
        const vl3 = data[row3_address + col3_offset];

        // perform filtering in X (rows)
        const s0 = interpolate_bicubic(xd, vi0, vi1, vi2, vi3);
        const s1 = interpolate_bicubic(xd, vj0, vj1, vj2, vj3);
        const s2 = interpolate_bicubic(xd, vk0, vk1, vk2, vk3);
        const s3 = interpolate_bicubic(xd, vl0, vl1, vl2, vl3);

        // filter in Y (columns)
        return interpolate_bicubic(yd, s0, s1, s2, s3);
    }

    /**
     *
     * @param {number} u
     * @param {number} v
     * @param {number[]|Float32Array} result
     * @param {number} result_offset
     */
    sampleBilinearUV(u, v, result, result_offset = 0) {

        const itemSize = this.itemSize;

        for (let i = 0; i < itemSize; i++) {
            result[i + result_offset] = this.sampleChannelBilinearUV(u, v, i);

        }
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @param {number[]|Float32Array|Float64Array} result
     * @param {number} result_offset
     */
    sampleBilinear(x, y, result, result_offset = 0) {

        const itemSize = this.itemSize;

        for (let i = 0; i < itemSize; i++) {
            //TODO this can be optimized greatly
            result[i + result_offset] = this.sampleChannelBilinear(x, y, i);

        }
    }

    /**
     *
     * @param {number} u
     * @param {number} v
     * @param {number} channel
     * @return {number}
     */
    sampleChannelBilinearUV(u, v, channel) {
        const x = u * this.width - 0.5;
        const y = v * this.height - 0.5;

        return this.sampleChannelBilinear(x, y, channel);
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @param {number} channel
     * @returns {number}
     */
    sampleChannelBilinear(x, y, channel) {
        assert.isNumber(x, 'x');
        assert.notNaN(x, 'x');

        assert.isNumber(y, 'y');
        assert.notNaN(y, 'y');

        assert.isNonNegativeInteger(channel, 'channel');

        const itemSize = this.itemSize;

        const width = this.width;
        const height = this.height;

        const rowSize = width * itemSize;

        //sample 4 points
        const x_max = width - 1;
        const y_max = height - 1;

        const clamped_x = clamp(x, 0, x_max);
        const clamped_y = clamp(y, 0, y_max);

        const x0 = clamped_x >>> 0;
        const y0 = clamped_y >>> 0;

        //
        const row0 = y0 * rowSize;
        const col0_offset = x0 * itemSize + channel;

        const i0 = row0 + col0_offset;

        //
        let x1, y1;

        if (clamped_x === x0) {
            x1 = x0;
        } else {
            x1 = x0 + 1;
        }


        if (clamped_y === y0) {
            y1 = y0;
        } else {
            y1 = y0 + 1;
        }

        const data = this.data;

        const q0 = data[i0];

        if (x0 === x1 && y0 === y1) {
            // exactly sampled in the center of the pixel, no interpolation required
            return q0;
        }

        //
        const xd = clamped_x - x0;
        const yd = clamped_y - y0;

        const col1_offset = x1 * itemSize + channel;

        const i1 = row0 + col1_offset;

        const row1 = y1 * rowSize;

        const j0 = row1 + col0_offset;
        const j1 = row1 + col1_offset;

        const q1 = data[i1];
        const p0 = data[j0];
        const p1 = data[j1];

        // perform Bi-Linear interpolation
        const s0 = lerp(q0, q1, xd);
        const s1 = lerp(p0, p1, xd);

        return lerp(s0, s1, yd);
    }

    /**
     *
     * @param {number} u
     * @param {number} v
     * @param {number[]|ArrayLike<number>} result
     */
    sampleNearestUV(u, v, result) {
        const w = this.width;
        const h = this.height;

        const x = Math.round(u * w - 0.5);
        const y = Math.round(v * h - 0.5);

        this.read(
            clamp(x, 0, w - 1),
            clamp(y, 0, h - 1),
            result
        );
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @param {number} channel
     * @returns {number}
     */
    readChannel(x, y, channel) {
        assert.isNumber(x, "x");
        assert.isNumber(y, "y");
        assert.isNumber(channel, "channel");

        assert.isNonNegativeInteger(channel, 'channel');
        assert.lessThan(channel, this.itemSize);

        const index = (y * this.width + x) * this.itemSize + channel;

        return this.data[index];
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @param {number[]} result
     */
    read(x, y, result) {

        const width = this.width;

        const itemSize = this.itemSize;

        const i0 = (y * width + x) * itemSize;

        for (let i = 0; i < itemSize; i++) {
            result[i] = this.data[i0 + i];
        }
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @param {number[]|ArrayLike<number>} texel
     */
    write(x, y, texel) {

        const width = this.width;

        const itemSize = this.itemSize;

        const i0 = (y * width + x) * itemSize;

        for (let i = 0; i < itemSize; i++) {
            this.data[i0 + i] = texel[i];
        }

        this.version++;
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @returns {number}
     */
    point2index(x, y) {
        return x + y * this.width;
    }

    /**
     *
     * @param {number} index
     * @param {Vector2} result
     */
    index2point(index, result) {
        const width = this.width;

        const x = index % width;
        const y = (index / width) | 0;

        result.set(x, y);
    }

    /**
     * Copy a patch from another sampler
     * @param {Sampler2D} source where to copy from
     * @param {Number} sourceX where to start reading from, X coordinate
     * @param {Number} sourceY where to start reading from, X coordinate
     * @param {Number} destinationX where to start writing to, X coordinate
     * @param {Number} destinationY where to start writing to, X coordinate
     * @param {Number} width size of the patch that is to be copied
     * @param {Number} height size of the patch that is to be copied
     */
    copy(
        source, sourceX, sourceY,
        destinationX, destinationY, width, height
    ) {

        assert.isNumber(sourceX, 'sourceX');
        assert.isNumber(sourceY, 'sourceY');

        assert.isNumber(destinationX, 'destinationX');
        assert.isNumber(destinationY, 'destinationY');

        assert.isNumber(width, 'width');
        assert.isNumber(height, 'height');

        const _w = Math.min(width, source.width - sourceX, this.width - destinationX);
        const _h = Math.min(height, source.height - sourceY, this.height - destinationY);


        const dItemSize = this.itemSize;
        const sItemSize = source.itemSize;
        const _itemSize = Math.min(dItemSize, sItemSize);


        const dRowSize = dItemSize * this.width;
        const sRowSize = sItemSize * source.width;

        const sData = source.data;
        const dData = this.data;

        let x, y, i;

        for (y = 0; y < _h; y++) {
            const dA = (y + destinationY) * dRowSize;
            const sA = (y + sourceY) * sRowSize;
            for (x = 0; x < _w; x++) {
                const dOffset = dA + (x + destinationX) * dItemSize;
                const sOffset = sA + (x + sourceX) * sItemSize;
                for (i = 0; i < _itemSize; i++) {
                    dData[dOffset + i] = sData[sOffset + i];
                }
            }
        }

        this.version++;
    }


    /**
     * Fill data values with zeros for a given area
     * Specialized version of `fill` method, optimized for speed
     * @param {Number} x
     * @param {Number} y
     * @param {Number} width
     * @param {Number} height
     */
    zeroFill(x, y, width, height) {
        assert.isNonNegativeInteger(width, 'width');
        assert.isNonNegativeInteger(height, 'height');

        const x0 = clamp(x, 0, this.width);
        const y0 = clamp(y, 0, this.height);
        const x1 = clamp(x + width, 0, this.width);
        const y1 = clamp(y + height, 0, this.height);

        // console.log(`#zerofill x:${x}, y:${y}, width:${width}, height:${height} \t -> \t x0:${x0}, y0:${y0}, x1:${x1}, y1:${y1}`); // DEBUG

        const data = this.data;
        const itemSize = this.itemSize;

        const rowSize = itemSize * this.width;

        const clearRowOffset0 = x0 * itemSize;
        const clearRowOffset1 = x1 * itemSize;

        let _y;

        for (_y = y0; _y < y1; _y++) {

            const a = _y * rowSize;

            data.fill(0, a + clearRowOffset0, a + clearRowOffset1);

        }

        this.version++;
    }

    /**
     *
     * @param {number} channel_index
     * @param {number} value
     */
    channelFill(channel_index, value) {
        const itemSize = this.itemSize;
        const data = this.data;
        const length = data.length;

        for (let i = channel_index; i < length; i += itemSize) {
            data[i] = value;
        }

        this.version++;
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @param {number} width
     * @param {number} height
     * @param {Array.<number>} value
     */
    fill(x, y, width, height, value) {

        const _w = this.width;
        const _h = this.height;

        const x0 = clamp(x, 0, _w);
        const y0 = clamp(y, 0, _h);
        const x1 = clamp(x + width, 0, _w);
        const y1 = clamp(y + height, 0, _h);

        const data = this.data;
        const itemSize = this.itemSize;

        const rowSize = itemSize * _w;

        let _y, _x, i;

        for (_y = y0; _y < y1; _y++) {

            const a = _y * rowSize;

            for (_x = x0; _x < x1; _x++) {

                const offset = a + _x * itemSize;

                for (i = 0; i < itemSize; i++) {

                    data[offset + i] = value[i];

                }

            }
        }

        this.version++;
    }

    /**
     * Set channel value of a specific texel
     * @param {number} x
     * @param {number} y
     * @param {number} channel
     * @param {number} value
     */
    writeChannel(x, y, channel, value) {
        assert.isNumber(x, "x");
        assert.isNumber(y, "y");

        assert.greaterThanOrEqual(x, 0);
        assert.greaterThanOrEqual(y, 0);
        assert.lessThan(x, this.width);
        assert.lessThan(y, this.height);

        assert.isNonNegativeInteger(channel, 'channel');
        assert.lessThan(channel, this.itemSize);

        const pointIndex = y * this.width + x;
        const pointAddress = pointIndex * this.itemSize;
        const channelAddress = pointAddress + channel;

        this.data[channelAddress] = value;

        this.version++;
    }


    /**
     * Traverses area inside a circle
     * NOTE: Based on palm3d answer on stack overflow: https://stackoverflow.com/questions/1201200/fast-algorithm-for-drawing-filled-circles
     * @param {number} centerX
     * @param {number} centerY
     * @param {number} radius
     * @param {function(x:number,y:number, sampler:Sampler2D)} visitor
     */
    traverseCircle(centerX, centerY, radius, visitor) {
        let x, y;

        //convert offsets to integers for safety
        const offsetX = centerX | 0;
        const offsetY = centerY | 0;

        const r2 = radius * radius;

        const radiusCeil = Math.ceil(radius);

        for (y = -radiusCeil; y <= radiusCeil; y++) {
            const y2 = y * y;

            for (x = -radiusCeil; x <= radiusCeil; x++) {

                if (x * x + y2 <= r2) {
                    visitor(offsetX + x, offsetY + y, this);
                }

            }
        }
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @param {boolean} [preserveData=true]
     */
    resize(x, y, preserveData = true) {
        assert.isNonNegativeInteger(x, 'x');
        assert.isNonNegativeInteger(y, 'y');

        const _w = this.width;
        const _h = this.height;

        if (_w === x && _h === y) {
            // size didn't change
            return;
        }

        const itemSize = this.itemSize;

        const length = x * y * itemSize;

        const oldData = this.data;

        const Constructor = typedArrayConstructorByInstance(oldData);

        const newData = new Constructor(length);

        if (preserveData) {
            //copy old data
            if (x === _w) {
                // number of columns is preserved, we can just copy the old data across
                newData.set(oldData.subarray(0, Math.min(oldData.length, length)));
            } else {
                //we need to copy new data row-by-row
                const rowCount = min2(y, _h);

                const columnCount = min2(x, _w);

                for (let i = 0; i < rowCount; i++) {
                    for (let j = 0; j < columnCount; j++) {

                        const targetItemAddress = (i * x + j) * itemSize;
                        const sourceItemAddress = (i * _w + j) * itemSize;

                        for (let k = 0; k < itemSize; k++) {

                            newData[targetItemAddress + k] = oldData[sourceItemAddress + k];

                        }
                    }
                }
            }
        }

        this.width = x;
        this.height = y;

        this.data = newData;

        this.version++;
    }

    /**
     * Estimate memory requirement of the object
     * @return {number}
     */
    computeByteSize() {
        let dataSize;

        const data = this.data;

        if (Array.isArray(data)) {
            // Assume IEEE float 64
            dataSize = 8 * data.length;
        } else {
            dataSize = data.byteLength;
        }

        return dataSize + 280;
    }

    /**
     *
     * @param {Sampler2D} other
     * @return {boolean}
     */
    equals(other) {
        if (other === undefined || other === null) {
            // should not happen, but just in case
            return false;
        }

        if (this === other) {
            // special case
            return true;
        }

        if (
            this.width !== other.width
            || this.height !== other.height
            || this.itemSize !== other.itemSize
        ) {
            return false;
        }

        return is_typed_array_equals(this.data, other.data);
    }

    /**
     *
     * @return {number}
     */
    hash() {
        const item_size = this.itemSize;
        const width = this.width;
        const height = this.height;
        const data = this.data;

        let hash = (((width & 0xFFFF) << 16) | (height & 0xFFFF)) ^ item_size;

        const texel_count = width * height;
        const element_count = texel_count * item_size;

        // we want to hash all channels for each chosen texel

        if (isTypedArray(data)) {

            hash ^= array_buffer_hash(data.buffer, data.byteOffset, data.byteLength);

        } else {
            // floating point texture

            for (let i = 0; i < element_count; ++i) {

                const channel_value = data[i];

                hash = ((hash << 5) - hash) + computeHashFloat(channel_value);

            }
        }

        return hash;
    }


    /**
     * @returns {Sampler2D}
     */
    clone() {
        let data_clone;

        if (Array.isArray(this.data)) {
            data_clone = this.data.slice();
        } else {
            // storage is a typed array
            const T = this.data.constructor;

            data_clone = new T(this.data);
        }

        return new Sampler2D(data_clone, this.itemSize, this.width, this.height);
    }

    toJSON() {
        const encoded = Base64.encode(this.data.buffer);

        return {
            height: this.height,
            width: this.width,
            itemSize: this.itemSize,
            type: compute_binary_data_type_from_typed_array(this.data),
            data: encoded
        }
    }

    fromJSON({ height, width, itemSize, type, data }) {
        const CTOR = compute_typed_array_constructor_from_data_type(type);

        if (typeof data === "string") {

            const decoded = Base64.decode(data);

            this.data = new CTOR(decoded);

        } else if (Array.isArray(data)) {
            // deprecated
            console.warn('Array JSON format is deprecated, please upgrade your data as soon as possible');

            this.data = new CTOR(data);

        } else {

            throw new Error('Unsupported data format');

        }

        this.height = height;
        this.width = width;
        this.itemSize = itemSize;
    }

    /**
     *
     * @param {number} itemSize
     * @param {number} width
     * @param {number} height
     * @return {Sampler2D}
     */
    static uint8clamped(itemSize, width, height) {
        const data = new Uint8ClampedArray(width * height * itemSize);
        return new Sampler2D(data, itemSize, width, height);
    }

    /**
     *
     * @param {number} itemSize
     * @param {number} width
     * @param {number} height
     * @return {Sampler2D}
     */
    static uint8(itemSize, width, height) {
        const data = new Uint8Array(width * height * itemSize);
        return new Sampler2D(data, itemSize, width, height);
    }

    /**
     *
     * @param {number} itemSize
     * @param {number} width
     * @param {number} height
     * @return {Sampler2D}
     */
    static uint16(itemSize, width, height) {
        const data = new Uint16Array(width * height * itemSize);
        return new Sampler2D(data, itemSize, width, height);
    }

    /**
     *
     * @param {number} itemSize
     * @param {number} width
     * @param {number} height
     * @return {Sampler2D}
     */
    static uint32(itemSize, width, height) {
        const data = new Uint32Array(width * height * itemSize);
        return new Sampler2D(data, itemSize, width, height);
    }

    /**
     *
     * @param {number} itemSize
     * @param {number} width
     * @param {number} height
     * @return {Sampler2D}
     */
    static int8(itemSize, width, height) {
        const data = new Int8Array(width * height * itemSize);
        return new Sampler2D(data, itemSize, width, height);
    }

    /**
     *
     * @param {number} itemSize
     * @param {number} width
     * @param {number} height
     * @return {Sampler2D}
     */
    static int16(itemSize, width, height) {
        const data = new Int16Array(width * height * itemSize);
        return new Sampler2D(data, itemSize, width, height);
    }

    /**
     *
     * @param {number} itemSize
     * @param {number} width
     * @param {number} height
     * @return {Sampler2D}
     */
    static int32(itemSize, width, height) {
        const data = new Int32Array(width * height * itemSize);
        return new Sampler2D(data, itemSize, width, height);
    }


    /**
     *
     * @param {number} itemSize
     * @param {number} width
     * @param {number} height
     * @return {Sampler2D}
     */
    static float16(itemSize, width, height) {
        const data = new Float16Array(width * height * itemSize);
        return new Sampler2D(data, itemSize, width, height);
    }

    /**
     *
     * @param {number} itemSize
     * @param {number} width
     * @param {number} height
     * @return {Sampler2D}
     */
    static float32(itemSize, width, height) {
        const data = new Float32Array(width * height * itemSize);
        return new Sampler2D(data, itemSize, width, height);
    }

    /**
     *
     * @param {number} itemSize
     * @param {number} width
     * @param {number} height
     * @return {Sampler2D}
     */
    static float64(itemSize, width, height) {
        const data = new Float64Array(width * height * itemSize);
        return new Sampler2D(data, itemSize, width, height);
    }

}

/**
 * @readonly
 * @type {boolean}
 */
Sampler2D.prototype.isSampler2D = true;

/**
 * @readonly
 * @type {string}
 */
Sampler2D.typeName = "Sampler2D";



