import { NEEDLE_progressive } from "@needle-tools/gltf-progressive";
import { Group, Mesh, Object3D, ShaderMaterial, Texture, Vector2, Vector4 } from "three";

import { MaterialPropertyBlock } from "../engine/engine_materialpropertyblock.js";
import type { Context } from "../engine/engine_setup.js";
import { getParam } from "../engine/engine_utils.js";
import { type Renderer } from "./Renderer.js";

const debug = getParam("debuglightmaps");

const $lightmapKey = Symbol("lightmapKey");


/**
 * This component is automatically added by the {@link Renderer} component if the object has lightmap uvs AND we have a lightmap.
 *
 * @category Rendering
 * @group Components
 */
export class RendererLightmap {

    get lightmap(): Texture | null {
        return this.lightmapTexture;
    }
    set lightmap(tex: Texture | null) {
        if (tex !== this.lightmapTexture) {
            this.lightmapTexture = tex;
            this.applyLightmap();
            this.updatePropertyBlockTexture();
            if (this.lightmapTexture) {
                NEEDLE_progressive.assignTextureLOD(this.lightmapTexture, 0).then((res: unknown) => {
                    if ((res as Texture)?.isTexture) {
                        this.lightmapTexture = res as Texture;
                        this.updatePropertyBlockTexture();
                    }
                })
            }
        }
    }

    private lightmapIndex: number = -1;
    private lightmapScaleOffset: Vector4 = new Vector4(1, 1, 0, 0);

    private readonly renderer: Renderer;
    private _isApplied: boolean = false;

    private get context(): Context { return this.renderer.context; }
    private get gameObject() { return this.renderer.gameObject; }

    private lightmapTexture: Texture | null = null;

    constructor(renderer: Renderer) {
        this.renderer = renderer;
    }

    init(lightmapIndex: number, lightmapScaleOffset: Vector4, lightmapTexture: Texture) {
        console.assert(this.gameObject !== undefined && this.gameObject !== null, "Missing gameobject", this);
        this.lightmapIndex = lightmapIndex;
        if (this.lightmapIndex < 0) return;
        this.lightmapScaleOffset = lightmapScaleOffset;
        this.lightmapTexture = lightmapTexture;
        NEEDLE_progressive.assignTextureLOD(lightmapTexture, 0).then((res: unknown) => {
            if ((res as Texture)?.isTexture) {
                this.lightmapTexture = res as Texture;
                this.updatePropertyBlockTexture();
            }
        })
        if (debug == "show") {
            console.log("Lightmap:", this.gameObject.name, lightmapIndex, "\nScaleOffset:", lightmapScaleOffset, "\nTexture:", lightmapTexture)
            this.setLightmapDebugMaterial();
        }
        else if (debug) console.log("Use debuglightmaps=show to render lightmaps only in the scene.")
        this.applyLightmap();
    }

    updateLightmapUniforms(_material: any) {
    }

    /**
     * Apply the lightmap to the object using MaterialPropertyBlock instead of cloning materials.
     * The lightmap texture and its per-object UV transform are set as overrides via PropertyBlock.
     * Three.js reads material.lightMap to determine shader defines and upload uniforms,
     * and uses texture.offset/repeat to compute lightMapTransform in the vertex shader.
     */
    applyLightmap() {
        if (this._isApplied) return;

        if (this.gameObject.type === "Object3D") {
            if (debug)
                console.warn("Can not add lightmap. Is this object missing a renderer?", this.gameObject.name);
            return;
        }

        const mesh = this.gameObject as unknown as (Mesh | Group);
        this.ensureLightmapUvs(mesh);

        if (this.lightmapIndex >= 0 && this.lightmapTexture) {
            this.lightmapTexture.channel = 1;
            const so = this.lightmapScaleOffset;
            for (let i = 0; i < this.renderer.sharedMaterials.length; i++) {
                const mat = this.renderer.sharedMaterials[i];
                if (!mat) continue;

                // If the material doesn't support lightmaps, skip it
                if (mat["lightMap"] === undefined) continue;

                if (debug) console.log("Setting lightmap on material", mat.name, "for renderer", this.renderer.name);

                // Use property block to set lightMap with per-object UV transform.
                // The texture transform differentiates cache keys per object and
                // PropertyBlock handles save/restore of the shared texture's offset/repeat.
                const propertyBlock = MaterialPropertyBlock.get(this.gameObject);
                propertyBlock.setOverride("lightMap", this.lightmapTexture, {
                    offset: new Vector2(so.z, 1 - so.y - so.w),
                    repeat: new Vector2(so.x, so.y)
                });

                (mat as any)[$lightmapKey] = true;
            }

            this._isApplied = true;
        }
    }

    /** Update the lightMap override on all property blocks (e.g. after LOD swap) */
    private updatePropertyBlockTexture() {
        if (!this._isApplied || !this.lightmapTexture) return;
        this.lightmapTexture.channel = 1;
        const so = this.lightmapScaleOffset;
        const propertyBlock = MaterialPropertyBlock.get(this.gameObject);
        propertyBlock.setOverride("lightMap", this.lightmapTexture, {
            offset: new Vector2(so.z, 1 - so.y - so.w),
            repeat: new Vector2(so.x, so.y)
        });
    }

    /**
     * Remove the lightmap from the object
     */
    onUnset() {
        for (let i = 0; i < this.renderer.sharedMaterials.length; i++) {
            const mat = this.renderer.sharedMaterials[i];
            if (mat) {
                delete (mat as any)[$lightmapKey];
            }
        }
        const block = MaterialPropertyBlock.get(this.gameObject);
        if (block) {
            block.removeOveride("lightMap");
        }
    }

    private ensureLightmapUvs(object: Object3D | Group | Mesh) {
        if (object instanceof Mesh) {
            if (!object.geometry.getAttribute("uv1")) {
                object.geometry.setAttribute("uv1", object.geometry.getAttribute("uv"));
            }
        }
        else if (object instanceof Group) {
            for (const child of object.children) {
                this.ensureLightmapUvs(child);
            }
        }
    }

    private setLightmapDebugMaterial() {
        const so = this.lightmapScaleOffset;

        // debug lightmaps
        this.gameObject["material"] = new ShaderMaterial({
            vertexShader: `
                varying vec2 vUv1;
                void main()
                {
                    vUv1 = uv1;
                    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
                }
                `,
            fragmentShader: `
                uniform sampler2D lightMap;
                uniform float lightMapIntensity;
                varying vec2 vUv1;

                // took from threejs 05fc79cd52b79e8c3e8dec1e7dca72c5c39983a4
                vec4 conv_sRGBToLinear( in vec4 value ) {
                    return vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.a );
                }

                void main() {
                    vec2 lUv = vUv1.xy * vec2(${so.x.toFixed(6)}, ${so.y.toFixed(6)}) + vec2(${so.z.toFixed(6)}, ${(1 - so.y - so.w).toFixed(6)});

                    vec4 lightMapTexel = texture2D( lightMap, lUv);
                    gl_FragColor = lightMapTexel;
                    gl_FragColor.a = 1.;
                }
                `,
            defines: { USE_LIGHTMAP: '' }
        });
    }
}
