import { AmbientLight, Color, HemisphereLight, Object3D } from "three";
import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";

import { Behaviour, GameObject } from "../../engine-components/Component.js";
import { ContextEvent, ContextRegistry } from "../engine_context_registry.js";
import { Mathf } from "../engine_math.js";
import { AmbientMode, DefaultReflectionMode } from "../engine_scenelighting.js";
import { Context } from "../engine_setup.js";
import { type SourceIdentifier } from "../engine_types.js";
import { getParam } from "../engine_utils.js";
import { LightmapType } from "./NEEDLE_lightmaps.js";

export const EXTENSION_NAME = "NEEDLE_lighting_settings";
const debug = getParam("debugenvlight");

export type LightingSettings = {
    ambientMode: AmbientMode;
    ambientIntensity: number,
    ambientLight: number[],
    ambientTrilight: Array<number[]>,
    environmentReflectionSource: DefaultReflectionMode;
}

export class NEEDLE_lighting_settings implements GLTFLoaderPlugin {

    get name(): string {
        return EXTENSION_NAME;
    }

    private parser: GLTFParser;
    private sourceId: SourceIdentifier;
    private context: Context;

    constructor(parser: GLTFParser, sourceId: SourceIdentifier, context: Context) {
        this.parser = parser;
        this.sourceId = sourceId;
        this.context = context;
    }

    afterRoot(_result: GLTF): Promise<void> | null {

        const extensions = this.parser.json.extensions;
        if (extensions) {
            const ext: LightingSettings = extensions[EXTENSION_NAME];
            if (ext) {
                if (debug)
                    console.log("Loaded \"" + this.name + "\", src: \"" + this.sourceId + "\"", ext);
                let settings: SceneLightSettings | undefined = undefined;
                // If the result scene has only one child we add the LightingSettingsComponent to that child
                if (_result.scene.children.length === 1) {
                    const obj = _result.scene.children[0];
                    // add a component to the root of the scene
                    settings = GameObject.addComponent(obj, SceneLightSettings, {}, { callAwake: false });
                }
                // if the scene already has multiple children we add it as a new object
                else {
                    const lightSettings = new Object3D();
                    lightSettings.name = "LightSettings " + this.sourceId;
                    _result.scene.add(lightSettings);
                    settings = GameObject.addComponent(lightSettings, SceneLightSettings, {}, { callAwake: false });
                }
                settings.sourceId = this.sourceId;
                settings.ambientIntensity = ext.ambientIntensity;
                settings.ambientLight = new Color().fromArray(ext.ambientLight);
                if (Array.isArray(ext.ambientTrilight))
                    settings.ambientTrilight = ext.ambientTrilight.map(c => new Color().fromArray(c));
                settings.ambientMode = ext.ambientMode;
                settings.environmentReflectionSource = ext.environmentReflectionSource;
            }
        }
        return null;
    }

}

ContextRegistry.registerCallback(ContextEvent.ContextCreated, e => {
    const ctx = e.context as Context;
    const lightingSettings = GameObject.findObjectOfType(SceneLightSettings, ctx as Context);
    if (lightingSettings?.sourceId) lightingSettings.enabled = true;
})

// exists once per gltf scene root (if it contains reflection)
// when enabled it does currently automatically set the reflection
// this might not be desireable
export class SceneLightSettings extends Behaviour {

    ambientMode: AmbientMode = AmbientMode.Skybox;
    ambientLight?: Color;
    ambientTrilight?: Color[];
    ambientIntensity: number = 1;
    environmentReflectionSource: DefaultReflectionMode = DefaultReflectionMode.Skybox;

    private _hasReflection: boolean = false;
    private _ambientLightObj?: AmbientLight;
    private _hemisphereLightObj?: HemisphereLight;

    awake() {
        if (this.sourceId) {
            const type = this.environmentReflectionSource === DefaultReflectionMode.Skybox ? LightmapType.Skybox : LightmapType.Reflection;
            const tex = this.context.lightmaps.tryGet(this.sourceId, type, 0);
            this._hasReflection = tex !== null && tex !== undefined;
            if (tex)
                this.context.sceneLighting.internalRegisterReflection(this.sourceId, tex);
        }

        this.enabled = false;
        this.context.sceneLighting.internalRegisterSceneLightSettings(this);

        if (debug) {
            window.addEventListener("keydown", evt => {
                if(this.destroyed) return;
                switch (evt.key) {
                    case "l":
                        this.enabled = !this.enabled;
                        break;
                }
            });
        }

        // make sure the component is in the end of the list 
        // (e.g. if we have an animation on the first component from an instance and add the scenelightingcomponent the animation binding will break)
        const comps = this.gameObject.userData?.components;
        if (comps) {
            const index = comps.indexOf(this);
            comps.splice(index, 1);
            comps.push(this);
        }
    }

    onDestroy(): void {
        this.context.sceneLighting.internalUnregisterSceneLightSettings(this);
    }

    private calculateIntensityFactor(col:Color){
        const intensity = Math.max(col.r, col.g, col.b);// * 0.2126 + col.g * 0.7152 + col.b * 0.0722;
        const factor = 2.2 * Mathf.lerp(0, 1.33, intensity); // scale based on intensity
        return factor;
    }

    onEnable(): void {
        if (debug) console.warn("💡🟡 >>> Enable lighting", this.sourceId, this.enabled, this);

        if (this.ambientMode == AmbientMode.Flat) {
            if (this.ambientLight && !this._ambientLightObj) {
                // TODO: currently ambient intensity is always exported as 1? The exported values are not correct in threejs
                // the following calculation is a workaround to get the correct intensity
                const factor = this.calculateIntensityFactor(this.ambientLight);
                this._ambientLightObj = new AmbientLight(this.ambientLight, this.ambientIntensity * factor);
                if (debug) console.log("Created ambient light", this.sourceId, this._ambientLightObj, this.ambientIntensity, factor)
            }
            if (this._ambientLightObj) {
                this.gameObject.add(this._ambientLightObj)
            }
        }
        else if (this.ambientMode === AmbientMode.Trilight) {
            if (this.ambientTrilight) {
                const ground = this.ambientTrilight[0];
                const sky = this.ambientTrilight[this.ambientTrilight.length - 1];
                const factor = this.calculateIntensityFactor(sky);
                this._hemisphereLightObj = new HemisphereLight(sky, ground, this.ambientIntensity * factor);
                this.gameObject.add(this._hemisphereLightObj)
                if (debug) console.log("Created hemisphere ambient light", this.sourceId, this._hemisphereLightObj, this.ambientIntensity, factor)
            }
        }
        else {
            if (this._ambientLightObj)
                this._ambientLightObj.removeFromParent();
            if (this._hemisphereLightObj)
                this._hemisphereLightObj.removeFromParent();
        }

        if (this.sourceId)
            this.context.sceneLighting.internalEnableReflection(this.sourceId);
    }

    onDisable() {
        if (debug)
            console.warn("💡⚫ <<< Disable lighting:", this.sourceId, this);
        if (this._ambientLightObj)
            this._ambientLightObj.removeFromParent();
        if (this._hemisphereLightObj)
            this._hemisphereLightObj.removeFromParent();
        if (this.sourceId)
            this.context.sceneLighting.internalDisableReflection(this.sourceId);
    }
}