import { ClampToEdgeWrapping, DataTexture, NearestFilter, RedFormat, UnsignedByteType } from "three";
import { BinaryDataType } from "../../../core/binary/type/BinaryDataType.js";
import { assert } from "../../../core/assert.js";
import {
    DataType2TypedArrayConstructorMapping
} from "../../../core/binary/type/DataType2TypedArrayConstructorMapping.js";
import { max2 } from "../../../core/math/max2.js";
import { NumericType } from "../../../core/math/NumericType.js";
import { computeBinaryDataTypeByPrecision } from "../../../core/binary/type/computeBinaryDataTypeByPrecision.js";
import { computeThreeTextureTypeFromDataType } from "./computeThreeTextureTypeFromDataType.js";
import { computeThreeTextureInternalFormatFromDataType } from "./computeThreeTextureInternalFormatFromDataType.js";
import { computeThreeTextureFormat } from "./computeThreeTextureFormat.js";
import { DataTypeByteSizes } from "../../../core/binary/type/DataTypeByteSizes.js";

/**
 * How wide a data texture is
 * @type {number}
 */
const DATA_TEXTURE_WIDTH = 128;

const SHRINK_FACTOR = 0.5;
const GROW_FACTOR = 1.05;

/**
 * Mainly used for storing non-color data in GPU memory
 * Useful for things like simulation and running general purpose GPU compute inside fragment shaders
 */
export class TextureBackedMemoryRegion {

    constructor() {
        /**
         *
         * @type {number}
         * @private
         */
        this.__channel_count = 1;

        this.size = 1;
        /**
         *
         * @type {number}
         * @private
         */
        this.__precision = 8;

        /**
         *
         * @type {NumericType|number}
         */
        this.__type = NumericType.Uint;

        /**
         *
         * @type {DataTexture}
         * @private
         */
        this.__texture = new DataTexture(
            new Uint8Array(0),
            0,
            0
        );

        // as of v128 THREE.js will ignore us and set texture width and height to 1, so we need to force it
        this.__texture.image.width = 0;
        this.__texture.image.height = 0;

        this.__texture.type = UnsignedByteType;
        this.__texture.format = RedFormat;
        this.__texture.magFilter = NearestFilter;
        this.__texture.minFilter = NearestFilter;
        this.__texture.wrapS = ClampToEdgeWrapping;
        this.__texture.wrapT = ClampToEdgeWrapping;
        this.__texture.generateMipmaps = false;

        /**
         *
         * @type {boolean}
         */
        this.needsRebuild = false;

        /**
         *
         * @type {number}
         * @private
         */
        this.__height = 0;

        /**
         *
         * @type {BinaryDataType|string}
         * @private
         */
        this.__data_type = BinaryDataType.Uint8;
    }

    get concrete_type() {
        return this.__data_type;
    }

    /**
     *
     * @param {number} v
     */
    set channel_count(v) {
        if (this.__channel_count === v) {
            // no change
            return;
        }

        this.__channel_count = v;

        this.needsRebuild = true;
    }

    get channel_count() {
        return this.__channel_count;
    }

    update() {
        if (this.needsRebuild) {
            this.build();
        }
    }

    /**
     *
     * @param {NumericType} t
     */
    set type(t) {
        assert.enum(t, NumericType, 't');

        this.__type = t;

        this.__update_data_type();
    }

    /**
     *
     * @return {NumericType|number}
     */
    get type() {
        return this.__type;
    }

    /**
     *
     * @return {number}
     */
    get precision() {
        return this.__precision;
    }

    /**
     *
     * @param {number} bit_count
     */
    set precision(bit_count) {
        assert.isNonNegativeInteger(bit_count, 'bit_count');

        this.__precision = bit_count;

        this.__update_data_type();
    }

    dispose() {
        this.__texture.dispose();
    }

    /**
     *
     * @private
     */
    __update_data_type() {
        const new_data_type = computeBinaryDataTypeByPrecision(this.__type, this.__precision);

        if (new_data_type !== this.__data_type) {
            this.__data_type = new_data_type;

            this.needsRebuild = true;
        }
    }

    /**
     * @private
     * @param {boolean} keep_data
     */
    build(keep_data = true) {
        const texture = this.__texture;

        const existing_data = texture.image.data;
        const existing_length = existing_data.length;

        const target_length = DATA_TEXTURE_WIDTH * this.__height * this.__channel_count;

        const TypedArray = DataType2TypedArrayConstructorMapping[this.__data_type];

        const new_data = new TypedArray(target_length);

        texture.format = computeThreeTextureFormat(this.__type, this.__channel_count);
        texture.type = computeThreeTextureTypeFromDataType(this.__data_type);
        texture.internalFormat = computeThreeTextureInternalFormatFromDataType(this.__data_type, this.channel_count);

        if (keep_data) {
            // copy existing data to new storage
            if (existing_length < target_length) {
                new_data.set(existing_data);
            } else {
                new_data.set(existing_data.slice(0, target_length));
            }
        }

        // dispose of the texture data on the GPU
        texture.dispose();

        // set new texture dimensions
        texture.image.height = this.__height;
        texture.image.width = DATA_TEXTURE_WIDTH;

        // assign new data
        texture.image.data = new_data;

        // mark texture for update
        texture.needsUpdate = true;

        // clear build flag
        this.needsRebuild = false;
    }

    /**
     *
     * @return {DataTexture}
     */
    getTexture() {
        return this.__texture;
    }

    /**
     *
     * @param {number} size
     * @returns {boolean} whether container was resized or not
     */
    resize(size) {
        assert.isNonNegativeInteger(size, 'size');

        this.size = size;

        const target_height = max2(
            Math.ceil(size / DATA_TEXTURE_WIDTH),
            1
        );

        const current_height = this.__texture.image.height;

        if (current_height < target_height) {
            //  texture too small
            //Over-provision to prevent thrashing
            this.__height = Math.ceil(target_height * GROW_FACTOR);
            this.needsRebuild = true;
            return true;
        } else if (current_height * SHRINK_FACTOR > target_height) {
            // texture too large, lets shrink it down
            this.__height = target_height;
            this.needsRebuild = true;
            return true;
        }

        // no actual container change
        return false;
    }

    /**
     *
     * @return {number}
     */
    getTextureByteSize() {
        const unit_byte_size = DataTypeByteSizes[this.__data_type];

        const image = this.__texture.image;

        return unit_byte_size * image.width * image.height * this.__channel_count;
    }
}
