import { assert } from "../../../../core/assert.js";
import { array_remove_first } from "../../../../core/collection/array/array_remove_first.js";
import { array_sort_quick } from "../../../../core/collection/array/array_sort_quick.js";
import Signal from "../../../../core/events/signal/Signal.js";
import { AssetManager } from "../../../asset/AssetManager.js";
import { GameAssetType } from "../../../asset/GameAssetType.js";
import { Sampler2D } from "../sampler/Sampler2D.js";
import { sampler2d_to_uint8_RGBA } from "../sampler/sampler2d_to_uint8_RGBA.js";
import { compose_finger_print } from "./tile/compose_finger_print.js";
import { decompose_finger_print } from "./tile/decompose_finger_print.js";
import { VirtualTextureTile } from "./tile/VirtualTextureTile.js";

/**
 * Facilitates prioritized queueing and loading of individual texture tiles
 */
export class VirtualTextureTileLoader {
    /**
     * Where the tiles are stored
     * @type {string}
     */
    #root_path = "";

    /**
     * Lets user specify a custom file name template
     * @type {function(mip:number, x:number, y:number):string}
     */
    #file_name_builder = (mip, x, y) => {

        assert.isNonNegativeInteger(mip, 'mip');
        assert.isNonNegativeInteger(x, 'x');
        assert.isNonNegativeInteger(y, 'y');

        return `${mip}-${x}-${y}.png`;

    }

    /**
     *
     * @return {function(number, number, number): string}
     */
    get file_name_builder() {
        return this.#file_name_builder;
    }

    /**
     *
     * @param {function(mip:number, x:number, y:number):string} builder
     */
    set file_name_builder(builder) {
        assert.isFunction(builder, 'builder');

        this.#file_name_builder = builder;
    }

    set path(v) {
        this.#root_path = v;
    }

    /**
     * When queue gets larger than this, we start discarding elements
     * @type {number}
     */
    queue_limit = Infinity;

    /**
     *
     * @type {AssetManager}
     */
    #asset_manager = null;

    set asset_manager(v) {
        this.#asset_manager = v;
    }

    /**
     * How many tiles can be loaded at the same time
     * @type {number}
     */
    #concurrency = 4;

    /**
     * Tiles that are currently being loaded
     * @readonly
     * @type {number[]}
     */
    #in_flight = [];

    /**
     * Contains fingerprints
     * @readonly
     * @type {number[]}
     */
    #queue = [];

    /**
     * @readonly
     */
    on = {
        /**
         * @type {Signal<VirtualTextureTile>}
         */
        loaded: new Signal()
    };


    /**
     *
     * @param {number} fingerprint
     * @returns {boolean}
     */
    #queue_remove(fingerprint) {
        assert.isNonNegativeInteger(fingerprint, 'fingerprint');

        const i = this.#queue.indexOf(fingerprint);

        if (i === -1) {
            return false;
        }

        this.#queue.splice(i, 1);

        return true;
    }

    #queue_add(fingerprint) {
        assert.isNonNegativeInteger(fingerprint, 'fingerprint');

        if (this.is_queued(fingerprint)) {
            return false;
        }

        this.#queue.push(fingerprint);

        return true;
    }

    /**
     *
     * @param {number} fingerprint
     */
    enqueue(fingerprint) {
        if (this.#in_flight.indexOf(fingerprint) !== -1) {
            return false;
        }

        if (this.#queue_add(fingerprint) === false) {
            return false;
        }

        this.#prod();

        return true;
    }

    /**
     *
     * @param {number} fingerprint
     * @returns {boolean}
     */
    is_queued(fingerprint) {
        assert.isNonNegativeInteger(fingerprint, 'fingerprint');

        return this.#queue.indexOf(fingerprint) !== -1;
    }

    /**
     *
     * @param {VirtualTextureUsage} usage
     */
    #sort_queue_by_usage(usage) {

        /**
         *
         * @param {number} fingerprint
         */
        const score = (fingerprint) => -usage.getCountByFingerprint(fingerprint);

        array_sort_quick(
            this.#queue,
            score, null,
            0, this.#queue.length - 1
        );

    }

    /**
     *
     * @param {VirtualTextureUsage} usage
     */
    update_usage(usage) {
        this.#sort_queue_by_usage(usage);

        // truncate the queue if necessary
        const queue_overflow = this.#queue.length - this.queue_limit;

        if (queue_overflow > 0) {
            this.#queue.splice(this.queue_limit, queue_overflow);
        }
    }

    /**
     * Initializes load of the next element in the queue if there is capacity
     */
    #prod() {
        while (this.#in_flight.length < this.#concurrency && this.#queue.length > 0) {
            const fingerprint = this.#queue[0];
            const { mip, x, y } = decompose_finger_print(fingerprint);

            this.#load(mip, x, y);
        }
    }

    /**
     *
     * @param {number} mip
     * @param {number} x
     * @param {number} y
     * @returns {Promise<VirtualTextureTile>}
     */
    #load(mip, x, y) {
        const file_name = this.#file_name_builder(mip, x, y);

        assert.isString(file_name, 'file_name');

        const finger_print = compose_finger_print(mip, x, y);

        this.#queue_remove(finger_print);
        this.#in_flight.push(finger_print);

        return this.#asset_manager.promise(`${this.#root_path}/${file_name}`, GameAssetType.Image)
            .then(asset => {

                const sampler = asset.create();

                const tile = new VirtualTextureTile();

                let rgba = sampler;

                if (rgba.itemSize !== 4) {
                    rgba = Sampler2D.uint8(4, sampler.width, sampler.height);

                    sampler2d_to_uint8_RGBA(rgba, sampler);
                }

                tile.data = rgba;
                tile.finger_print = finger_print;

                this.on.loaded.send1(tile);


                return tile;
            }, (reason) => {
                console.error(`Failed to load tile ${file_name}, reason:${reason}`);
            })
            .finally(() => {

                array_remove_first(this.#in_flight,finger_print);

                this.#prod();

            })

    }
}