import {
    ClampToEdgeWrapping,
    DataTexture,
    LinearFilter,
    NearestFilter,
    REVISION as THREE_VERSION,
    RGBAFormat,
    UnsignedByteType,
    Vector2
} from "three";
import { assert } from "../../../../core/assert.js";
import { BitSet } from "../../../../core/binary/BitSet.js";
import { Cache } from "../../../../core/cache/Cache.js";
import { array_copy } from "../../../../core/collection/array/array_copy.js";
import { HashMap } from "../../../../core/collection/map/HashMap.js";
import { passThrough } from "../../../../core/function/passThrough.js";
import { strictEquals } from "../../../../core/function/strictEquals.js";
import { max2 } from "../../../../core/math/max2.js";
import { min2 } from "../../../../core/math/min2.js";
import { Sampler2D } from "../sampler/Sampler2D.js";
import { writeSample2DDataToDataTexture } from "../sampler/writeSampler2DDataToDataTexture.js";
import { tile_address_to_finger_print } from "./tile/tile_address_to_finger_print.js";
import { VirtualTextureTileLoader } from "./VirtualTextureTileLoader.js";
import { VT_DEFAULT_PAGE_RESOLUTION } from "./VT_DEFAULT_PAGE_RESOLUTION.js";

/**
 * How much extra data to store in cache (in bytes)
 * IMPORTANT: MAKE SURE THAT THE CACHE CAN HOLD AT LEAST 1 TILE!
 * @type {number}
 */
const DEFAULT_CACHE_SIZE = 256 * 1024 * 1024;

export class VirtualTexturePage {
    /**
     * @readonly
     * @type {DataTexture}
     */
    #page_texture = new DataTexture(
        new Uint8Array(VT_DEFAULT_PAGE_RESOLUTION * VT_DEFAULT_PAGE_RESOLUTION * 4),
        VT_DEFAULT_PAGE_RESOLUTION,
        VT_DEFAULT_PAGE_RESOLUTION,
        RGBAFormat,
        UnsignedByteType,
    );

    #tile_copy_sampler = Sampler2D.uint8(4, 1, 1);
    #tile_copy_texture = new DataTexture(this.#tile_copy_sampler.data, 1, 1);

    #page_texture_size = [VT_DEFAULT_PAGE_RESOLUTION, VT_DEFAULT_PAGE_RESOLUTION];

    /**
     *
     * @type {number[]}
     */
    #page_texture_resolution_in_tiles = [0, 0];

    get page_texture_resolution_in_tiles() {
        return this.#page_texture_resolution_in_tiles;
    }

    /**
     * Used internally to track number of times `update` method was called
     * @type {number}
     */
    update_count = 0;

    /**
     * Track updates to page content, whenever a tile is made resident or is removed - this number goes up by 1
     * @type {number}
     */
    version = 0;

    /**
     * Maximum number of new page assignments per single update cycle
     * This helps prevent thrashing and keep performance overhead of updating pages predictable
     * @type {number}
     */
    #cycle_assignment_limit = 4;

    /**
     *
     * @type {THREE.WebGLRenderer|null}
     */
    #renderer = null;

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


    get page_texture_resolution() {
        return this.#page_texture_size;
    }

    get texture() {
        return this.#page_texture;
    }

    /**
     *
     * @type {VirtualTextureTile[]}
     */
    #resident_tiles = [];

    /**
     *
     * @returns {VirtualTextureTile[]}
     */
    get resident_tiles() {
        return this.#resident_tiles;
    }

    /**
     *
     * @type {HashMap<number, VirtualTextureTile>}
     */
    #resident_tile_lookup = new HashMap({
        keyHashFunction: passThrough,
        keyEqualityFunction: strictEquals,
    });

    #page_slot_occupancy = new BitSet();

    #tile_resolution = 128;

    get tile_resolution() {
        return this.#tile_resolution;
    }

    /**
     *
     * @param {number} v
     */
    set tile_resolution(v) {
        this.#tile_resolution = v;

        this.#update_tile_slot_resolution();
        this.clear_resident();
    }

    /**
     * Extra padding on repeated pixels around each tile on each side (top, left, right, bottom)
     * Used to enable texture filtering
     * @type {number}
     */
    #tile_margin = 4;

    get tile_margin() {
        return this.#tile_margin;
    }

    #tile_slot_resolution = this.#tile_resolution + this.#tile_margin * 2;

    #update_tile_slot_resolution() {
        this.#tile_slot_resolution = this.#tile_resolution + this.#tile_margin * 2;
    }

    #residency_tile_capacity = 0;

    /**
     * Must be >=0
     * When 0 - tiles that would not fit onto the page will not be loaded
     * When >0 - same fraction of extra tiles will be fetching and put into cache, resulting in prefetching
     * @type {number}
     */
    #prefetch_factor = 0.33;

    #loader = new VirtualTextureTileLoader();

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

    #page_texture_set_parameters() {

        const texture = this.#page_texture;

        texture.unpackAlignment = 4;

        texture.type = UnsignedByteType;
        texture.format = RGBAFormat;
        texture.internalFormat = "RGBA8";

        texture.generateMipmaps = false;

        texture.minFilter = NearestFilter;
        texture.magFilter = LinearFilter;

        texture.wrapS = ClampToEdgeWrapping;
        texture.wrapT = ClampToEdgeWrapping;

        texture.anisotropy = this.#tile_margin * 2;

    }

    constructor() {
        this.#page_texture.name = "Virtual Texture / Page";
        this.#page_texture_set_parameters();
        this.#page_texture.needsUpdate = true;

        this.#tile_copy_texture.name = "Virtual Texture / Copy Tile";

        this.#tile_copy_texture.wrapT = ClampToEdgeWrapping;
        this.#tile_copy_texture.wrapS = ClampToEdgeWrapping;
        this.#tile_copy_texture.generateMipmaps = false;


        this.#loader.on.loaded.add(this.#handle_tile_loaded, this);

        this.page_texture_size = [VT_DEFAULT_PAGE_RESOLUTION, VT_DEFAULT_PAGE_RESOLUTION];
    }

    /**
     *
     * @param {VirtualTextureTile} tile
     */
    #handle_tile_loaded(tile) {
        const finderPrint = tile.finger_print;

        assert.notNull(tile.data, 'tile.data');
        assert.equal(tile.data.width, this.#tile_slot_resolution);
        assert.equal(tile.data.height, this.#tile_slot_resolution);

        if (this.#tile_cache.has(finderPrint)) {
            // already loaded
            return;
        }

        this.#tile_cache.put(finderPrint, tile);

    }

    /**
     *
     * @param {string} v
     */
    set path(v) {
        this.#loader.path = v;
    }

    /**
     *
     * @param {AssetManager} v
     */
    set asset_manager(v) {
        this.#asset_manager = v

        this.#loader.asset_manager = v;
    }

    #update_residency_capacity() {
        const slot_resolution = this.#tile_slot_resolution;
        const size = this.#page_texture_size;

        const x = Math.floor(size[0] / slot_resolution);
        const y = Math.floor(size[1] / slot_resolution);

        this.#page_texture_resolution_in_tiles[0] = x;
        this.#page_texture_resolution_in_tiles[1] = y;

        this.#residency_tile_capacity = x * y;

        // Limit the queue to be able to load no more than an entire page, trying to load more would be wasteful as we wouldn't be able to use that data immediately anyway
        this.#loader.queue_limit = max2(1, this.#residency_tile_capacity);
    }


    /**
     * Tiles that are not currently resident live here, this is essentially a second level cache
     * key is the tile's "fingerprint"
     * @type {Cache<number,VirtualTextureTile>}
     */
    #tile_cache = new Cache({
        keyHashFunction: passThrough,
        keyEqualityFunction: strictEquals,
        maxWeight: DEFAULT_CACHE_SIZE,
        valueWeigher: () => this.#tile_resolution * this.#tile_resolution * 4
    });

    #find_eviction_target_index() {
        // find least-recently used
        const tiles = this.#resident_tiles;
        const count = tiles.length;

        let min = Infinity;
        let min_index = -1;

        for (let i = 0; i < count; i++) {
            const tile = tiles[i];

            if (tile.last_used_time < min) {
                min = tile.last_used_time;
                min_index = i;
            }
        }

        return min_index;
    }

    #evict_one() {
        const index = this.#find_eviction_target_index();

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

        return this.#remove_resident_tile(index);
    }

    /**
     *
     * @param {number} index
     * @returns {boolean}
     */
    #remove_resident_tile(index) {
        const [removed] = this.#resident_tiles.splice(index, 1);

        const pageSlot = removed.page_slot;

        // mark slot as empty for next tile
        this.#page_slot_occupancy.clear(pageSlot);

        removed.page_slot = -1;

        const fingerprint = removed.finger_print;

        this.#resident_tile_lookup.delete(fingerprint);

        // push into cache
        this.#tile_cache.put(fingerprint, removed);

        this.version++;

        return true;
    }

    /**
     *
     * @param {VirtualTextureTile} tile
     */
    #make_resident(tile) {

        if (this.#residency_tile_capacity === 0) {
            throw new Error('Capacity is 0');
        }

        let slot = this.#page_slot_occupancy.nextClearBit(0);

        while (slot >= this.#residency_tile_capacity) {
            this.#evict_one();

            slot = this.#page_slot_occupancy.nextClearBit(0);
        }

        tile.page_slot = slot;
        tile.last_used_time = this.update_count;

        this.#page_slot_occupancy.set(slot, true);
        this.#resident_tiles.push(tile);
        this.#resident_tile_lookup.set(tile.finger_print, tile);

        const res_x = this.#page_texture_resolution_in_tiles[0];

        const position_tile_x = slot % res_x;
        const position_tile_y = (slot - position_tile_x) / res_x;

        const slot_resolution = this.#tile_slot_resolution;

        const copy_sampler = this.#tile_copy_sampler;
        const tile_texture = this.#tile_copy_texture;
        if (copy_sampler.width !== slot_resolution || copy_sampler.height !== slot_resolution) {
            copy_sampler.resize(slot_resolution, slot_resolution);

            writeSample2DDataToDataTexture(copy_sampler, tile_texture);
        }

        tile_texture.image.data.set(tile.data.data);
        tile_texture.needsUpdate = true;

        const write_position = new Vector2(
            position_tile_x * slot_resolution,
            position_tile_y * slot_resolution,
        );

        const renderer = this.#renderer;
        if (THREE_VERSION < 165) {
            renderer.copyTextureToTexture(
                write_position, tile_texture, this.#page_texture
            );
        } else {
            // API changed since version 165
            renderer.copyTextureToTexture(
                tile_texture,
                this.#page_texture,
                // srcRegion
                {
                    min: { x: 0, y: 0 },
                    max: { x: slot_resolution, y: slot_resolution },
                },
                // dstPosition
                write_position
            );
        }

        this.version++;

        return true;
    }

    /**
     *
     * @param {number[]} v
     */
    set page_texture_size(v) {

        const current_resolution = this.#page_texture_size;

        if (v[0] !== current_resolution[0] || v[1] !== current_resolution[1]) {

            array_copy(
                v, 0,
                current_resolution, 0,
                2
            );

            this.clear_resident();

            const page_texture = this.#page_texture;
            const page_image = page_texture.image;

            page_texture.dispose();

            const width = current_resolution[0];
            const height = current_resolution[1];

            page_image.data = new Uint8Array(width * height * 4);
            page_image.width = width;
            page_image.height = height;

            page_texture.needsUpdate = true;

        }

        this.#update_residency_capacity();
    }

    get page_texture_size() {
        return this.#page_texture_size;
    }

    /**
     *
     * @param {VirtualTextureUsage} usage
     */
    update_usage(usage) {
        const usage_occupancy = usage.occupancy;
        const usage_occupancy_count = usage.occupancy_count;


        // Usage occupancy is already sorted by priority, so there's no point is trying to fetch more than t
        let fetch_limit = this.#residency_tile_capacity * (1 + this.#prefetch_factor);

        let writes_remaining = min2(
            max2(1, this.#cycle_assignment_limit),
            this.#residency_tile_capacity
        );

        let remaining_slots = this.#residency_tile_capacity;

        for (let i = 0; i < usage_occupancy_count && fetch_limit > 0; i++) {
            const tile_address = usage_occupancy[i];

            const fingerPrint = tile_address_to_finger_print(tile_address);

            let tile = this.#resident_tile_lookup.get(fingerPrint);

            if (tile !== undefined) {
                // already resident

                // touch to prevent eviction
                tile.last_used_time = this.update_count;
                remaining_slots--;
                fetch_limit--;

                continue;
            }

            tile = this.#tile_cache.get(fingerPrint);

            if (tile === null) {
                this.#loader.enqueue(fingerPrint);
            } else if (remaining_slots > 0) {
                // found in cache, and we have some slots remaining to write into

                if (writes_remaining > 0) {
                    // tile is not active, and we can write it
                    writes_remaining--;
                    this.#make_resident(tile);
                }

                fetch_limit--;
                remaining_slots--;
            }
        }


        this.#loader.update_usage(usage);

        this.update_count++;
    }

    clear_resident() {

        while (this.#resident_tiles.length > 0) {
            this.#evict_one();
        }

    }

    dispose() {
        this.clear_resident();

        this.#page_texture.dispose();
        this.#tile_cache.clear();
        this.#page_slot_occupancy.reset();

    }
}