import {
    ClampToEdgeWrapping,
    DataTexture,
    DataTexture2DArray,
    FloatType,
    LinearEncoding,
    LinearFilter,
    LinearMipMapLinearFilter,
    NearestFilter,
    RepeatWrapping,
    RGBAFormat,
    RGFormat,
    UnsignedByteType
} from "three";
import { assert } from "../../../../../core/assert.js";
import { Cache } from "../../../../../core/cache/Cache.js";
import { typed_array_copy } from "../../../../../core/collection/array/typed/typed_array_copy.js";
import List from "../../../../../core/collection/list/List.js";
import Vector2 from "../../../../../core/geom/Vector2.js";
import { invokeObjectEquals } from "../../../../../core/model/object/invokeObjectEquals.js";
import { invokeObjectHash } from "../../../../../core/model/object/invokeObjectHash.js";
import { computeStringHash } from "../../../../../core/primitives/strings/computeStringHash.js";
import { sampler2d_scale } from "../../../../graphics/texture/sampler/resize/sampler2d_scale.js";
import { Sampler2D } from "../../../../graphics/texture/sampler/Sampler2D.js";
import { sampler2d_ensure_uint8_RGBA } from "../../../../graphics/texture/sampler/sampler2d_ensure_uint8_RGBA.js";
import { TerrainLayer } from "./TerrainLayer.js";

class ScaledTextureKey {
    uri = '';

    size = new Vector2();

    /**
     *
     * @param {string} uri
     * @param {Vector2} size
     * @returns {ScaledTextureKey}
     */
    static from(uri, size) {
        assert.isString(uri, 'uri');

        const r = new ScaledTextureKey();

        r.uri = uri;
        r.size.copy(size);

        return r;
    }

    hash() {
        return computeStringHash(this.uri) ^ this.size.hash();
    }

    equals(other) {
        return this.uri === other.uri && this.size.equals(other.size);
    }
}

/**
 *
 * @type {Cache<ScaledTextureKey,Sampler2D>}
 */
const scaled_texture_cache = new Cache({
    maxWeight: 104857600, // 100 mb
    keyEqualityFunction: invokeObjectEquals,
    keyHashFunction: invokeObjectHash,
    /**
     *
     * @param {Sampler2D} sampler
     * @returns {number}
     */
    valueWeigher(sampler) {
        return sampler.computeByteSize();
    }
});

const DEFAULT_RESOLUTION = 512;

export class TerrainLayers {
    constructor() {

        /**
         *
         * @type {List<TerrainLayer>}
         */
        this.layers = new List();

        /**
         *
         * @type {Vector2}
         */
        this.resolution = new Vector2(DEFAULT_RESOLUTION, DEFAULT_RESOLUTION);


        const layer_data = new Uint8Array(4);
        layer_data.fill(255); //pre-fill with white

        /**
         *
         * @type {DataTexture2DArray}
         */
        this.texture = new DataTexture2DArray(layer_data, 1, 1, 1);
        this.texture.format = RGBAFormat;
        this.texture.type = UnsignedByteType;

        this.texture.wrapS = RepeatWrapping;
        this.texture.wrapT = RepeatWrapping;

        this.texture.anisotropy = 8;

        this.texture.magFilter = LinearFilter;
        this.texture.minFilter = LinearMipMapLinearFilter;

        this.texture.generateMipmaps = true;
        this.texture.encoding = LinearEncoding;
        this.texture.internalFormat = 'RGBA8';
        this.texture.needsUpdate = true;

        this.scalesTexture = new DataTexture(new Float32Array(64), 32, 1, RGFormat, FloatType);

        this.scalesTexture.wrapS = ClampToEdgeWrapping;
        this.scalesTexture.wrapT = ClampToEdgeWrapping;

        this.scalesTexture.minFilter = LinearFilter;
        this.scalesTexture.magFilter = NearestFilter;

        this.scalesTexture.internalFormat = 'RG32F';

        this.scalesTexture.generateMipmaps = false;
        this.scalesTexture.needsUpdate = true;
    }

    toJSON() {
        return {
            resolution: this.resolution.toJSON(),
            layers: this.layers.toJSON()
        };
    }

    fromJSON({ resolution = DEFAULT_RESOLUTION, layers = [] } = {}) {
        this.resolution.fromJSON(resolution);
        this.layers.fromJSON(layers, TerrainLayer);
    }

    /**
     *
     * @param {number} terrainWidth
     * @param {number} terrainHeight
     */
    updateLayerScales(terrainWidth, terrainHeight) {
        const terrainLayers = this.layers;
        const n = terrainLayers.length;

        const texture = this.scalesTexture;

        const image = texture.image;

        const scaleTextureDataLength = n * 2;

        if (image.data.length < scaleTextureDataLength) {
            // dispose of old texture data
            texture.dispose();

            image.data = new Float32Array(scaleTextureDataLength);

            image.height = 1;
            image.width = n;
        }

        const data = image.data;


        for (let i = 0; i < n; i++) {
            const layer = terrainLayers.get(i);

            const index2 = i * 2;

            const scale_x = (terrainWidth) / layer.size.x;
            const scale_y = (terrainHeight) / layer.size.y;


            data[index2] = scale_x;
            data[index2 + 1] = scale_y;
        }

        texture.needsUpdate = true;
    }

    /**
     *
     * @param {AssetManager} assetManager
     * @returns {Promise}
     */
    loadTextureData(assetManager) {
        const n = this.layers.length;

        const promises = [];

        for (let i = 0; i < n; i++) {
            const layer = this.layers.get(i);

            const promise = layer.loadTextureData(assetManager);

            const layerIndex = i;

            promise.then(
                () => {
                    this.writeLayerDataIntoTexture(layerIndex);
                },
                (reason) => {
                    console.error(`Failed to load layer [${layerIndex}]"${layer.textureDiffuseURL}". Reason: ${reason}`);
                }
            );

            promises.push(promise);
        }

        return Promise.all(promises);
    }

    writeAllLayersDataIntoTexture() {
        for (let i = 0; i < this.layers.length; i++) {
            this.writeLayerDataIntoTexture(i);
        }
    }

    /**
     *
     * @param {number} index
     * @return {TerrainLayer}
     */
    get(index) {
        return this.layers.get(index);
    }

    /**
     *
     * @param {TerrainLayerDescription} description
     * @returns {number}
     */
    findIndexByDescription(description) {
        assert.defined(description, 'description');

        const layers = this.layers;
        const n = layers.length;

        for (let i = 0; i < n; i++) {
            const terrainLayer = layers.get(i);

            if (
                terrainLayer.size.equals(description.size)
                && terrainLayer.textureDiffuseURL === description.diffuse
            ) {
                return i;
            }
        }

        // not found
        return -1;
    }

    /**
     *
     * @param {string} url
     * @returns {number}
     */
    findIndexByDiffuseTextureURL(url) {
        assert.isString(url, 'url');

        return this.layers.findIndex(t => t.textureDiffuseURL === url);
    }

    /**
     *
     * @return {number}
     */
    count() {
        return this.layers.length;
    }

    /**
     *
     * @param {TerrainLayer} layer
     * @returns {number}
     */
    addLayer(layer) {
        assert.defined(layer);
        assert.equal(layer.isTerrainLayer, true, 'layer.isTerrainLayer !== true');

        const index = this.layers.length;

        this.layers.add(layer);

        return index;
    }

    /**
     *
     * @param {TerrainLayer} layer
     * @return {boolean}
     */
    removeLayer(layer) {
        return this.layers.removeOneOf(layer);
    }

    /**
     *
     * @param {TerrainLayer} layer
     * @param {Vector2} resolution
     * @private
     * @returns {Sampler2D}
     */
    __obtain_layer_data_at_resolution(layer, resolution) {
        const layerSampler = layer.diffuse;

        assert.equal(layerSampler.itemSize, 4, 'layer sampler must have 4 channels');

        const resolution_x = resolution.x;
        const resolution_y = resolution.y;

        if (layerSampler.width === resolution_x && layerSampler.height === resolution_y) {
            //layer's data matches texture dimensions, we can just copy the data directly
            return layer.diffuse;
        } else {

            // check cache
            const key = ScaledTextureKey.from(layer.textureDiffuseURL, resolution);
            const existing = scaled_texture_cache.get(key);

            if (existing !== null) {
                return existing;
            }

            const scaledLayerSampler = Sampler2D.uint8(4, resolution_x, resolution_y);

            sampler2d_scale(layerSampler, scaledLayerSampler);

            // Ensure result is uint8 RGBA
            const rgba = sampler2d_ensure_uint8_RGBA(scaledLayerSampler);

            scaled_texture_cache.put(key, rgba);

            return rgba;
        }
    }

    /**
     *
     * @param {number} index
     */
    writeLayerDataIntoTexture(index) {
        /**
         *
         * @type {TerrainLayer}
         */
        const layer = this.get(index);

        const image = this.texture.image;

        const arrayData = image.data;


        const resolution = this.resolution;

        const single_layer_byte_size = resolution.x * resolution.y * 4;

        if (arrayData.length < single_layer_byte_size * index) {
            throw new Error('Texture data is too small, make sure you rebuild texture data before attempting to write');
        }

        const address = single_layer_byte_size * index;

        const sampler = this.__obtain_layer_data_at_resolution(layer, resolution);

        assert.equal(sampler.data.length, single_layer_byte_size, 'Incorrect layer data size');

        arrayData.set(sampler.data, address);

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

    dispose() {
        this.texture.dispose();
        this.scalesTexture.dispose();
    }

    clear() {
        this.layers.reset();
    }


    buildTexture() {
        const texture = this.texture;

        const image = texture.image;

        //figure out the largest layer size
        const size_x = this.resolution.x;
        const size_y = this.resolution.y;


        const terrainLayers = this.layers;

        const layerCount = terrainLayers.length;

        // glTexStorage3D requires all dims > 0; uploading a 0-depth array texture
        // produces GL_INVALID_VALUE. Leave the texture at its prior valid state
        // (1x1x1 white from the constructor) until at least one layer exists.
        if (size_x === 0 || size_y === 0 || layerCount === 0) {
            return;
        }

        if (image.width === size_x && image.height === size_y && image.depth === layerCount) {
            //already the right size
            return;
        }

        // dispose of the old texture data
        texture.dispose();

        //build an array to hold texture array
        const arrayData = new Uint8Array(size_x * size_y * 4 * layerCount);
        arrayData.fill(255); // fill with white

        if (image.width === size_x && image.height === size_y) {
            // dimensions preserved, copy old data
            typed_array_copy(image.data, arrayData);
        }

        image.data = arrayData;
        image.width = size_x;
        image.height = size_y;
        image.depth = layerCount;

        texture.needsUpdate = true;
    }
}
