import { ClampToEdgeWrapping, DataTexture, NearestFilter, RedIntegerFormat, UnsignedIntType } from "three";
import { assert } from "../../../../core/assert.js";
import { UINT32_MAX } from "../../../../core/binary/UINT32_MAX.js";
import { compose_tile_address } from "./tile/compose_tile_address.js";
import { decompose_finger_print } from "./tile/decompose_finger_print.js";

const EMPTY_TILE = UINT32_MAX;

/*
 ENCODING:
    22 bit  : slot index
    10 bit   : tile mip
 */

/**
 * Represents entire mip pyramid of tiles, where each tile slot points to an actual resident tile
 */
export class VirtualTextureMemoryMapping {
    /**
     * In tiles
     * @type {number}
     */
    #resolution = 0;
    #max_mip_level = 0;

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

    #pyramid = new Uint32Array(0);
    #texture = new DataTexture(
        new Uint32Array(1),
        1,
        1,
        RedIntegerFormat,
        UnsignedIntType
    )

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

    constructor() {
        const texture = this.#texture;
        texture.name = "Virtual Texture / Dereference Mapping"

        texture.unpackAlignment = 1;
        texture.format = RedIntegerFormat;
        texture.type = UnsignedIntType;

        texture.internalFormat = "R32UI";
        texture.generateMipmaps = false;

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

        texture.magFilter = NearestFilter;
        texture.minFilter = NearestFilter;
    }

    /**
     *
     * @param {number} resolution in tiles
     */
    set resolution(resolution) {
        assert.isNonNegativeInteger(resolution, 'resolution');

        if (this.#resolution === resolution) {
            return;
        }

        this.#resolution = resolution;

        //max mip
        const max_mip_level = Math.log2(resolution);

        const desired_size = compose_tile_address(max_mip_level + 1, 0, 0);

        const texture_dimension = Math.ceil(Math.sqrt(desired_size));

        const size = texture_dimension*texture_dimension;

        this.#max_mip_level = max_mip_level;

        const arrayBuffer = new ArrayBuffer(size * 4);

        this.#pyramid = new Uint32Array(arrayBuffer);

        const texture = this.#texture;
        texture.dispose();


        texture.image.data = this.#pyramid;
        texture.image.width = texture_dimension;
        texture.image.height = texture_dimension;

        texture.needsUpdate = true;
    }

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

    /**
     *
     * @param {VirtualTexturePage} page
     */
    set residency(page) {

        const tiles = page.resident_tiles;
        const tile_count = tiles.length;

        // clear pyramid

        const pyramid = this.#pyramid;

        pyramid.fill(EMPTY_TILE);

        // fill by residence
        for (let i = 0; i < tile_count; i++) {
            const tile = tiles[i];

            const fingerPrint = tile.finger_print;
            const { mip, x, y } = decompose_finger_print(fingerPrint);
            const address = compose_tile_address(mip, x, y);

            const encoded =
                (tile.page_slot & 0x3FFFFF)
                | ((mip & 0x3FF) << 22) //mip
            ;

            pyramid[address] = encoded;
        }

        // back-fill empty slots
        for (let mip = 1; mip <= this.#max_mip_level; mip++) {

            const mip_resolution = 1 << mip;

            for (let y = 0; y < mip_resolution; y++) {
                for (let x = 0; x < mip_resolution; x++) {

                    const tile_address = compose_tile_address(mip, x, y);

                    if (pyramid[tile_address] !== EMPTY_TILE) {
                        // page is filled, all is good, move on
                        continue;
                    }

                    const parent_mip = mip - 1;
                    const parent_x = x >>> 1;
                    const parent_y = y >>> 1;

                    const parent_tile_address = compose_tile_address(parent_mip, parent_x, parent_y);

                    const parent_encoded_tile = pyramid[parent_tile_address];

                    // decode parent tile
                    const tile_index = (parent_encoded_tile) & 0x3FFFFF
                    const tile_mip = (parent_encoded_tile >> 22) & 0x3FF

                    // swap in parent tile


                    const encoded_tile =
                        (tile_index)
                        | (tile_mip << 22)
                    ;

                    pyramid[tile_address] = encoded_tile;
                }
            }

        }

        // request texture update
        this.#texture.needsUpdate = true;
    }

    getTileMetadata(x,y,mip){

        const tile_address = compose_tile_address(mip, x, y);

        const encoded_tile = this.#pyramid[tile_address];

        return {
            slot: encoded_tile & 0x3FFFFF,
            mip: ((encoded_tile >> 22) & 0x3FF)
        };
    }


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