import type { LOD_Results } from "@needle-tools/gltf-progressive";
import { LODsManager as _LODsManager, NEEDLE_progressive, NEEDLE_progressive_plugin } from "@needle-tools/gltf-progressive";
import { Box3, Camera, Mesh, PerspectiveCamera, Scene, Sphere, WebGLRenderer } from "three";

import type { Context } from "./engine_context.js";
import { Gizmos } from "./engine_gizmos.js";
import { getTempVector } from "./engine_three_utils.js";
import { IGameObject } from "./engine_types.js";
import { getParam } from "./engine_utils.js";

const debug = getParam("debugprogressive");

const _tempBox: Box3 = new Box3();
const _tempSphere: Sphere = new Sphere();

/**
 * Needle Engine LODs manager. Wrapper around the internal LODs manager.
 * It uses the [@needle-tools/gltf-progressive](https://npmjs.com/package/@needle-tools/gltf-progressive) package to manage LODs.
 *
 * For lower-level control (e.g. configuring max concurrent loading tasks, queue settings, or other progressive loading specifics), use {@link NEEDLE_progressive} directly.
 * @link https://npmjs.com/package/@needle-tools/gltf-progressive
 */
export class LODsManager implements NEEDLE_progressive_plugin {

    /** The type of the @needle-tools/gltf-progressive LODsManager - can be used to set static settings */
    static readonly GLTF_PROGRESSIVE_LODSMANAGER_TYPE = _LODsManager;

    readonly context: Context;
    private _lodsManager?: _LODsManager;

    private _settings: Partial<Pick<_LODsManager, "skinnedMeshAutoUpdateBoundsInterval" | "targetTriangleDensity">> = {
    }

    /**
     * The internal LODs manager. See @needle-tools/gltf-progressive for more information.  
     * @link https://npmjs.com/package/@needle-tools/gltf-progressive
     */
    get manager() {
        return this._lodsManager;
    }

    /**
     * The interval (in seconds) at which the bounding volumes of skinned meshes are automatically updated.  
     * If set to 0, automatic updates are disabled and bounding volumes will only be updated when the mesh is loaded or when the `updateSkinnedMeshBounds` method is called manually.  
     * @default 0
     */
    get skinnedMeshAutoUpdateBoundsInterval() {
        return this._lodsManager?.skinnedMeshAutoUpdateBoundsInterval || this._settings.skinnedMeshAutoUpdateBoundsInterval || 0;
    }
    set skinnedMeshAutoUpdateBoundsInterval(value: number) {
        this._settings.skinnedMeshAutoUpdateBoundsInterval = value;
        this.applySettings();
    }

    /**
     * The target triangle density is the desired max amount of triangles on screen when the mesh is filling the screen.  
     * @default 200_000
     */
    get targetTriangleDensity() {
        return this._lodsManager?.targetTriangleDensity || this._settings.targetTriangleDensity || 200_000; // default value
    }
    set targetTriangleDensity(value: number) {
        this._settings.targetTriangleDensity = value;
        this.applySettings();
    }

    /** @internal */
    constructor(context: Context) {
        this.context = context;
    }

    private applySettings() {
        if (this._lodsManager) {
            for (const key in this._settings) {
                this._lodsManager[key] = this._settings[key];
            }
        }
    }

    /** @internal */
    setRenderer(renderer: WebGLRenderer) {
        this._lodsManager?.disable();
        _LODsManager.removePlugin(this);
        _LODsManager.addPlugin(this);
        _LODsManager.debugDrawLine = Gizmos.DrawLine;
        this._lodsManager = _LODsManager.get(renderer, { engine: "needle-engine" });
        this.applySettings();
        this._lodsManager.enable();
    }

    disable() {
        this._lodsManager?.disable();
        _LODsManager.removePlugin(this);
    }


    /** @internal */
    onAfterUpdatedLOD(_renderer: WebGLRenderer, _scene: Scene, camera: Camera, mesh: Mesh, level: LOD_Results): void {
        if (debug) this.onRenderDebug(camera, mesh, level);
    }

    private onRenderDebug(camera: Camera, mesh: Mesh, results: LOD_Results) {

        if (!mesh.geometry) return;
        if (!NEEDLE_progressive.hasLODLevelAvailable(mesh.geometry) && !NEEDLE_progressive.hasLODLevelAvailable(mesh.material)) return;

        const state = _LODsManager.getObjectLODState(mesh);
        if (!state) return;


        let level = results.mesh_lod;
        const changed = results.mesh_lod != state.lastLodLevel_Mesh || results.texture_lod != state.lastLodLevel_Texture;

        if (debug && mesh.geometry.boundingSphere) {
            const bounds = mesh.geometry.boundingSphere;
            _tempSphere.copy(bounds);
            _tempSphere.applyMatrix4(mesh.matrixWorld);
            const boundsCenter = _tempSphere.center;
            const radius = _tempSphere.radius;
            const colors = ["#76c43e", "#bcc43e", "#c4ac3e", "#c4673e", "#ff3e3e"];
            // if the lod has changed we just want to draw the gizmo for the changed mesh
            if (changed) {
                Gizmos.DrawWireSphere(boundsCenter, radius, colors[level], .1);
            }
            else {
                // Mesh Density is calculated as: triangle count per square meter of surface area, normalized to the bounding box size of the model.
                // Our goal for automatic switching of LODs is that the resulting triangle count per screen area is constant.
                // We assume a uniform distribution of triangles over the surface area; which means that
                // we can express a ratio of "screen area to surface area".
                const triangleCount = mesh.geometry.index?.count ?? 0 / 3;
                const lods = NEEDLE_progressive.getMeshLODExtension(mesh.geometry)?.lods;
                level = lods ? Math.min(lods?.length - 1, level) : 0;
                let allLods = "";
                if (lods && state.lastScreenCoverage > 0) {
                    for (let i = 0; i < lods.length; i++) {
                        const d = lods[i].density;
                        const last = i == lods.length - 1;
                        allLods += d.toFixed(0) + ">" + (d / state.lastScreenCoverage).toFixed(0) + (last ? "" : ",");
                    }
                }
                const density = lods ? lods[level]?.density : -1;

                // const box = mesh.geometry.boundingBox;
                // const boxSize = box ? box.getSize(getTempVector()) : new Vector3();
                // const maxBoxSize = Math.max(boxSize.x, boxSize.y, boxSize.z);

                // Surface area is in local space of the model; 
                // we need to scale it by the model's world scale and the model's geometry bounding box size.
                // const ws = mesh.getWorldScale(getTempVector());
                // const wsMedian = (ws.x + ws.y + ws.z) / 3;
                // Area is squared, so both maxBoxSize and wsMedian are squared here
                // Here, we're basically reverting the calculations that have happened in the pipeline for debugging.
                // const surfaceArea = 1 / density * triangleCount * (maxBoxSize * maxBoxSize) * (wsMedian * wsMedian);
                let text = "LOD " + results.mesh_lod + "\nTEX " + results.texture_lod;
                if (debug == "density") {
                    text +=
                        "\n" + triangleCount + " tris" +
                        // This is key – basically how we're switching
                        "\n" + (density / state.lastScreenCoverage).toFixed(0) + " dens" +
                        "\n" + (state.lastScreenCoverage * 100).toFixed(1) + "% cov" +
                        // "\n" + (this._lastScreenspaceVolume.x.toFixed(2) + "x" + this._lastScreenspaceVolume.y.toFixed(2) + "x" + this._lastScreenspaceVolume.z.toFixed(2)) + " vol" + 
                        // + "\n" + (surfaceArea).toFixed(2) + " m2" +
                        "\n" + (state.lastCentrality * 100).toFixed(1) + "% centr" +
                        "\n" + (_tempBox.min.x.toFixed(2) + "-" + _tempBox.max.x.toFixed(2) + "x" + _tempBox.min.y.toFixed(2) + "-" + _tempBox.max.y.toFixed(2)) + " scr" +
                        // "\n" + (ws.x).toFixed(2) + "x" + " " + maxBoxSize.toFixed(2) + "b" + "\n" + 
                        // allLods + "\n" +
                        //"----" + "\n" +
                        // "1000" + " ideal dens"
                        "";
                }

                // if (helper) {
                //     helper?.setText(text);
                //     continue;
                // }
                if (state.lastScreenCoverage > .1) {
                    const cam = camera as any as IGameObject;
                    const camForward = cam.worldForward;
                    const camWorld = cam.worldPosition;

                    const fwd = getTempVector(camForward);
                    // for debugging very close LDOs, we need to flip the radius...
                    const pos = fwd.multiplyScalar(radius * .7).add(boundsCenter);
                    const distance = pos.distanceTo(camWorld);
                    // const vertexCount = mesh.geometry.index!.count / 3;
                    // const vertexCountFactor = Math.min(1, vertexCount / 1000);
                    const col = colors[Math.min(colors.length - 1, Math.max(0, level))] + "88";
                    // const size = Math.min(10, radius);
                    const windowScale = this.context.domHeight > 0 ? screen.height / this.context.domHeight : 1;
                    const fieldOfViewScale = (camera as PerspectiveCamera).isPerspectiveCamera ? Math.tan((camera as PerspectiveCamera).fov * Math.PI / 180 / 2) : 1;
                    Gizmos.DrawLabel(pos, text, distance * .012 * windowScale * fieldOfViewScale, undefined, 0xffffff, col);
                }
            }

        }
    }

}