import {
    ClampToEdgeWrapping,
    DoubleSide,
    GLSL3,
    Matrix4,
    NearestFilter,
    NoBlending,
    RawShaderMaterial,
    RGBAIntegerFormat,
    UnsignedByteType,
    Vector2,
    Vector4,
    WebGLRenderTarget
} from "three";
import { assert } from "../../../../core/assert.js";
import { array_copy } from "../../../../core/collection/array/array_copy.js";
import { clamp } from "../../../../core/math/clamp.js";
import { max2 } from "../../../../core/math/max2.js";
import { generate_halton_jitter } from "../../../../core/math/random/generate_halton_jitter.js";
import { renderScreenSpace } from "../../render/utils/renderScreenSpace.js";
import { Sampler2D } from "../sampler/Sampler2D.js";
import { VirtualTextureUsage } from "./VirtualTextureUsage.js";
import { fragment, vertex } from "./VirtualTextureUsageShader.js";

const usage_material_uniforms = {
    "u_mt_params": { value: new Vector2(0, 1) },
    /**
     * Format: VT_WIDTH, VT_HEIGHT, VT_TILE_SIZE, VT_ID
     *   - VT_WIDTH :       width of the virtual texture
     *   - VT_HEIGHT :      height of the virtual texture
     *   - VT_TILE_SIZE :   resolution of a single tile
     *   - VT_ID :          multiple different virtual textures may be used, this identifies current texture
     */
    "u_mt_tex": { value: new Vector4(1, 1, 1, 3) },
    "u_mt_viewport_scale": { value: new Vector2(1, 1) },
    "modelViewMatrix": { value: new Matrix4() },
    "projectionMatrix": { value: new Matrix4() }
};
const usage_material = new RawShaderMaterial({

    uniforms: usage_material_uniforms,
    vertexShader: vertex(),
    fragmentShader: fragment(),
    side: DoubleSide,
    glslVersion: GLSL3

});
usage_material.blending = NoBlending;


const clear_usage_material = new RawShaderMaterial({
    depthTest: false,
    depthWrite: false,
    vertexShader: `    
        precision mediump float;    
        in vec2 uv;
        void main() {
               
            gl_Position = vec4( (uv - 0.5)*2.0, 0.0, 1.0 );
            
        }`,
    fragmentShader: `
    
    precision mediump int;
    out uvec4 out_decoded;
        
    void main(){
        out_decoded =  uvec4(0u);
    }
    `,
    glslVersion: GLSL3
});

const scratch_matrix = new Float32Array(16);

export class VirtualTextureUsageUpdater {

    #frame_index = 0;

    /**
     * In order to avoid aliasing due to the fact that we render at a lower resolution, we move the camera slightly each frame to sweep across texels in-between
     * @type {Float32Array}
     */
    #frame_jitter_offsets = new Float32Array([0, 0]);
    #frame_jitter_samples = 1;
    #frame_jitter_enabled = false;

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

    /**
     *
     * @param {boolean} v
     */
    set frame_jitter_enabled(v) {
        this.#frame_jitter_enabled = v;
    }

    #initialize_frame_jitter() {
        this.#frame_jitter_samples = this.#usage_resolution_scale;
        this.#frame_jitter_offsets = generate_halton_jitter(this.#frame_jitter_samples);
    }

    #usage_buffer = new WebGLRenderTarget(1, 1, {
        wrapT: ClampToEdgeWrapping,
        wrapS: ClampToEdgeWrapping,
        minFilter: NearestFilter,
        magFilter: NearestFilter,
        format: RGBAIntegerFormat,
        type: UnsignedByteType,
        internalFormat: "RGBA8UI",
        anisotropy: 1,
        depthBuffer: true,
        stencilBuffer: false,
        generateMipmaps: false,
    });

    #usage_pixel_data = Sampler2D.uint8(4, 1, 1);

    #viewport_resolution = [1, 1];

    /**
     *
     * @type {number}
     */
    #usage_resolution_scale = 32;

    #usage_metadata = new VirtualTextureUsage();

    #texture_id = 3;
    #texture_resolution = 65536;
    #tile_resolution = 256;
    #max_mip_level = 0;

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

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

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

    /**
     * Used to fetch higher/lower resolution tiles, negative value will fetch higher resolution tiles, and vice-versa
     * @type {number}
     */
    #usage_texture_bias = 0;


    /**
     *
     * @param {number} resolution
     * @param {number} tile_size
     * @param {number} texture_id
     */
    setTextureParameters(
        resolution,
        tile_size,
        texture_id
    ) {
        assert.isNonNegativeInteger(resolution,'resolution');
        assert.lessThanOrEqual(tile_size, resolution, `tile_size(=${tile_size}) must be <= than the overall texture resolution(=${resolution})`);

        this.#texture_id = texture_id;
        this.#texture_resolution = resolution;
        this.#tile_resolution = tile_size;
        this.#max_mip_level = Math.log2(this.#texture_resolution / this.#tile_resolution);

        if (!Number.isInteger(this.#max_mip_level)) {
            throw new Error(`texture resolution must be a power-of-two multiple of tile resolution`);
        }

        this.#initialize_usage_uniforms();

        this.#usage_metadata.max_mip_level = this.#max_mip_level;
    }

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

    get usage_data() {
        return this.#usage_pixel_data;
    }

    constructor() {

        const target = this.#usage_buffer;
        target.scissorTest = false;


        this.#initialize_usage_uniforms();
        this.#initialize_frame_jitter();

    }

    #initialize_usage_uniforms() {

        const uniforms = usage_material.uniforms;

        const max_mip_level = this.#max_mip_level;


        uniforms.u_mt_params.value.set(
            this.#usage_texture_bias,
            max_mip_level
        );

        uniforms.u_mt_tex.value.set(
            this.#texture_resolution,
            this.#texture_resolution,
            this.#tile_resolution,
            this.#texture_id
        );
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     */
    setViewportResolution(x, y) {
        assert.isNumber(x, 'x');
        assert.isNumber(y, 'y');

        assert.notNaN(x, 'x');
        assert.notNaN(y, 'y');

        const _x = max2(1, Math.floor(x));
        const _y = max2(1, Math.floor(y));

        const viewport = this.#viewport_resolution;

        if (_x === viewport[0] && _y === viewport[1]) {
            // no change, do nothing
            return;
        }

        viewport[0] = _x;
        viewport[1] = _y;

        const usage_resolution_x = clamp(Math.floor(_x / this.#usage_resolution_scale), 1, _x);
        const usage_resolution_y = clamp(Math.floor(_y / this.#usage_resolution_scale), 1, _y);

        this.#usage_buffer.setSize(usage_resolution_x, usage_resolution_y);
        this.#usage_pixel_data.resize(usage_resolution_x, usage_resolution_y, true);

        const usage_material_uniforms = usage_material.uniforms;
        usage_material_uniforms.u_mt_viewport_scale.value.set(usage_resolution_x / _x, usage_resolution_y / _y);

    }

    #prepareUsageUniforms(camera) {

        const uniforms = usage_material.uniforms;
        const usage_buffer = this.#usage_buffer;
        const camera_projection_matrix = camera.projectionMatrix.elements;

        if (this.#frame_jitter_enabled) {
            // apply jitter to projection matrix
            const jitter_index = this.#frame_index % this.#frame_jitter_samples;

            const jitter_index_2 = jitter_index * 2;

            const jitter_offset_x = this.#frame_jitter_offsets[jitter_index_2];
            const jitter_offset_y = this.#frame_jitter_offsets[jitter_index_2 + 1];

            camera_projection_matrix[8] = jitter_offset_x / usage_buffer.width;
            camera_projection_matrix[9] = jitter_offset_y / usage_buffer.height;
        }


        uniforms.modelViewMatrix.value.multiplyMatrices(camera.matrixWorldInverse, camera.matrixWorld);
        uniforms.projectionMatrix.value.copy(camera.projectionMatrix);
    }

    /**
     *
     * @param {WebGLRenderer} renderer
     * @param {Scene} scene
     * @param {Camera} camera
     */
    updateUsage(renderer, scene, camera) {

        const usage_buffer = this.#usage_buffer;

        const camera_projection_matrix = camera.projectionMatrix.elements;

        // remember existing state
        const _old_render_target = renderer.getRenderTarget();
        const _auto_clear = renderer.autoClear;

        array_copy(camera_projection_matrix, 0, scratch_matrix, 0, 16);

        const _old_material = scene.overrideMaterial;
        const _old_camera_matrix_update = camera.matrixAutoUpdate;

        camera.matrixAutoUpdate = false;

        renderer.autoClear = false;
        renderer.setRenderTarget(usage_buffer);

        // clear out usage texture
        renderScreenSpace(renderer, clear_usage_material);

        renderer.clearDepth();

        scene.overrideMaterial = usage_material;

        this.#prepareUsageUniforms(camera);

        renderer.render(scene, camera);


        renderer.readRenderTargetPixels(
            usage_buffer, 0, 0,
            usage_buffer.width, usage_buffer.height,
            this.#usage_pixel_data.data
        );

        // restore state
        scene.overrideMaterial = _old_material;

        renderer.setRenderTarget(_old_render_target);
        renderer.autoClear = _auto_clear;
        array_copy(scratch_matrix, 0, camera_projection_matrix, 0, 16);

        camera.matrixAutoUpdate = _old_camera_matrix_update;

        //
        this.#usage_metadata.fromTexture(this.#usage_pixel_data.data);
        this.#usage_metadata.promoteAncestors(1);
        this.#usage_metadata.sortOccupancyByLOD();

        this.#frame_index++;
    }

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