import { NEEDLE_progressive } from "@needle-tools/gltf-progressive";
import { Material, Mesh, MeshPhysicalMaterial, ShaderMaterial, Texture, Vector4, WebGLProgramParametersWithUniforms } from "three";

import type { Context } from "../engine/engine_setup.js";
import { getParam } from "../engine/engine_utils.js";

const debug = getParam("debuglightmaps");

declare type MaterialWithLightmap = Material & { lightMap?: Texture | null };

// this component is automatically added by the Renderer if the object has lightmap uvs AND we have a lightmap
// for multimaterial objects GLTF exports a "Group" with the renderer component
// and every child mesh is a material from unity
export class RendererLightmap {

    get lightmap(): Texture | null {
        return this.lightmapTexture;
    }
    set lightmap(tex: Texture | null) {
        if (tex !== this.lightmapTexture) {
            this.lightmapTexture = tex;
            this.applyLightmap();
            if (this.lightmapTexture) {
                NEEDLE_progressive.assignTextureLOD(this.lightmapTexture, 0).then(res => {
                    if ((res as Texture)?.isTexture) this.lightmapTexture = res as Texture;
                })
            }
        }
    }

    private lightmapIndex: number = -1;
    private lightmapScaleOffset: Vector4 = new Vector4(1, 1, 0, 0);

    private context: Context;
    private gameObject: Mesh;
    private lightmapTexture: Texture | null = null;
    private lightmapScaleOffsetUniform = { value: new Vector4(1, 1, 0, 0) };
    private lightmapUniform: { value: Texture | null } = { value: null };

    constructor(gameObject: Mesh, context: Context) {
        this.gameObject = gameObject;
        this.context = context;
    }

    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 => {
            if ((res as Texture)?.isTexture) this.lightmapTexture = res as Texture;
        })
        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: Material) {
        const uniforms = material["uniforms"];
        if (uniforms && uniforms.lightmap) {
            this.lightmapScaleOffsetUniform.value = this.lightmapScaleOffset;
            uniforms.lightmapScaleOffset = this.lightmapScaleOffsetUniform;
        }
    }

    /**
     * Apply the lightmap to the object. This will clone the material and set the lightmap texture and scale/offset
     */
    applyLightmap() {
        if (this.gameObject.type === "Object3D") {
            if (debug)
                console.warn("Can not add lightmap. Is this object missing a renderer?", this.gameObject.name);
            return;
        }

        if (this.gameObject.type === "Group") {
            if (this.gameObject["Needle:Multimaterial-LightmapWarning"] === undefined) {
                this.gameObject["Needle:Multimaterial-LightmapWarning"] = true;
                console.warn("Lightmap on multimaterial object is not supported yet... please open a feature request on https://github.com/needle-tools/needle-engine-support if your project requires it");
            }
            return;
        }

        console.assert(this.gameObject.type === "Mesh", "Lightmap only works on meshes", this);

        const mesh = this.gameObject as unknown as Mesh;
        if (!mesh.geometry.getAttribute("uv1"))
            mesh.geometry.setAttribute("uv1", mesh.geometry.getAttribute("uv"));

        if (Array.isArray(this.gameObject.material)) {
            const mats: Material[] = this.gameObject.material;
            for (let i = 0; i < mats.length; i++) {
                mats[i] = this.ensureLightmapMaterial(mats[i]);
            }
        }
        else {
            this.gameObject.material = this.ensureLightmapMaterial(this.gameObject.material);
        }

        if (this.lightmapIndex >= 0 && this.lightmapTexture) {
            // always on channel 1 for now. We could optimize this by passing the correct lightmap index along
            this.lightmapTexture.channel = 1;
            const mat = this.gameObject.material;
            if (Array.isArray(mat)) {
                for (const entry of mat) {
                    this.assignLightmapTexture(entry as any);

                }
            }
            else if (mat) {
                this.assignLightmapTexture(mat);
            }
        }
    }

    private ensureLightmapMaterial(material: Material) {
        if (!material.userData) material.userData = {};
        // if (material instanceof MeshPhysicalMaterial) {
        //     return material;
        // }
        // check if the material version has changed and only then clone the material
        if (material["NEEDLE:lightmap-material-version"] != material.version) {
            if (material["NEEDLE:lightmap-material-version"] == undefined) {
                if (debug) console.warn("Cloning material for lightmap " + material.name);
                const mat: Material = material.clone();
                material = mat;
                material.onBeforeCompile = this.onBeforeCompile;
            }
            else {
                // we need to clone the material
            }
        }
        return material;
    }

    private assignLightmapTexture(material: MaterialWithLightmap) {
        if (!material) return;
        if (material instanceof MeshPhysicalMaterial && material.transmission > 0) {
            return;
        }
        const hasChanged = material.lightMap !== this.lightmapTexture || material["NEEDLE:lightmap-material-version"] !== material.version;
        if (!hasChanged) {
            return;
        }

        if (debug) console.log("Assigning lightmap", material.name, material.version);

        // assign the lightmap
        material.lightMap = this.lightmapTexture;
        // store the version of the material
        material["NEEDLE:lightmap-material-version"] = material.version;
    }

    private onBeforeCompile = (shader: WebGLProgramParametersWithUniforms, _) => {
        if (debug) console.log("Lightmaps, before compile\n", shader)
        this.lightmapScaleOffsetUniform.value = this.lightmapScaleOffset;
        this.lightmapUniform.value = this.lightmapTexture;
        shader.uniforms.lightmapScaleOffset = this.lightmapScaleOffsetUniform;
    }

    private setLightmapDebugMaterial() {

        // 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;
                uniform vec4 lightmapScaleOffset;
                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 * lightmapScaleOffset.xy + vec2(lightmapScaleOffset.z, (1. - (lightmapScaleOffset.y + lightmapScaleOffset.w)));
                    
                    vec4 lightMapTexel = texture2D( lightMap, lUv);
                    gl_FragColor = lightMapTexel;
                    gl_FragColor.a = 1.;
                }
                `,
            defines: { USE_LIGHTMAP: '' }
        });
    }
}