import { AssetReference, type ProgressCallback } from "../engine/engine_addressables.js";
import { InstantiateOptions } from "../engine/engine_gameobject.js";
import { InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
import { serializable } from "../engine/engine_serialization_decorator.js";
import { getParam } from "../engine/engine_utils.js";
import { Behaviour } from "../engine-components/Component.js";
import { EventList } from "./EventList.js";

const debug = getParam("debugnestedgltf");

/**
 * NestedGltf loads and instantiates a glTF file when the component starts.   
 * NestedGltf components are created by the Unity exporter when nesting Objects with the GltfObject component (in Unity).  
 * Use this for lazy-loading content, modular scene composition, or dynamic asset loading.  
 * 
 * ![](https://cloud.needle.tools/-/media/lJKrr_2tWlqRFdFc46U4bQ.gif)
 *
 * The loaded glTF is instantiated as a sibling (child of parent) by default,  
 * inheriting the transform of the GameObject with this component.  
 *
 * **Features:**  
 * - Automatic loading on start
 * - Progress callbacks for loading UI
 * - Preloading support for faster display
 * - Event callback when loading completes
 *
 * @example Load a glTF when object becomes active
 * ```ts
 * const nested = myPlaceholder.addComponent(NestedGltf);
 * nested.filePath = new AssetReference("models/furniture.glb");
 * nested.loaded.addEventListener(({ instance }) => {
 *   console.log("Loaded:", instance.name);
 * });
 * ```
 *
 * @example Preload for instant display
 * ```ts
 * // Preload during loading screen
 * await nested.preload();
 * // Later, when object becomes active, it displays instantly
 * ```
 *
 * @summary Loads and instantiates a nested glTF file
 * @category Asset Management
 * @group Components
 * @see {@link AssetReference} for asset loading utilities
 * @see {@link SceneSwitcher} for scene-level loading
 * @link https://engine.needle.tools/samples/hotspots
 */
export class NestedGltf extends Behaviour {

    /** Reference to the glTF file to load. Can be a URL or asset path. */
    @serializable(AssetReference)
    filePath?: AssetReference;

    /**
     * Event fired when the glTF has been loaded and instantiated.
     * Provides the component, loaded instance, and asset reference.
     */
    @serializable(EventList)
    loaded: EventList<{ component: NestedGltf, instance: any, asset: AssetReference }> = new EventList();

    /**
     * EXPERIMENTAL for cloud asset loading
     */
    @serializable()
    loadAssetInParent: boolean = true;


    private _isLoadingOrDoneLoading: boolean = false;

    /** Register a callback that will be called when the progress of the loading changes */
    listenToProgress(evt: ProgressCallback) {
        this.filePath?.beginListenDownload(evt)
    }

    /** Begin loading the referenced gltf file in filePath */
    preload() {
        return this.filePath?.preload() || null;
    }

    /** @internal */
    async start() {
        if (this._isLoadingOrDoneLoading) return;
        if (debug) console.log(this, this.guid);

        const parent = this.gameObject.parent;
        if (parent) {

            if (this.filePath) {

                this._isLoadingOrDoneLoading = true;
                const opts = new InstantiateOptions();
                // we need to provide stable guids for creating nested gltfs
                opts.idProvider = new InstantiateIdProvider(this.hash(this.guid));
                opts.parent = this.loadAssetInParent !== false ? parent : this.gameObject;
                this.gameObject.updateMatrix();
                const matrix = this.gameObject.matrix;
                if (debug) console.log("Load nested:", this.filePath?.url ?? this.filePath, this.gameObject.position);
                const res = await this.filePath?.instantiate?.call(this.filePath, opts);
                if (debug) console.log("Nested loaded:", this.filePath?.url ?? this.filePath, res);
                if (res && this.loadAssetInParent !== false) {
                    res.matrixAutoUpdate = false;
                    res.matrix.identity();
                    res.applyMatrix4(matrix);
                    res.matrixAutoUpdate = true;
                    res.layers.disableAll();
                    res.layers.set(this.layer);
                    this.loaded.invoke({ component: this, instance: res, asset: this.filePath });
                }
                if (debug) console.log("Nested loading done:", this.filePath?.url ?? this.filePath, res);
            }

        }
    }

    /** @internal */
    onDestroy(): void {
        // When the NestedGLTF gets destroyed we assume the loaded glTF is also destroyed
        // meaning the resources, textures etc are disposed
        // When this NestedGLTF would now be loaded again the AssetReference would still be in the loaded state
        // so it would instantiate without textures etc...
        // Perhaps we want to add a dispose callback or event method too?
        // Somehow we have to clean the AssetReference state
        this.filePath?.unload();
    }


    private hash(str: string): number {
        let hash = 0;
        for (let i = 0; i < str.length; i++) {
            hash = str.charCodeAt(i) + ((hash << 5) - hash);
        }
        return hash;
    }
}