import { Loader } from "three";
import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";

import { isDevEnvironment } from "../debug/index.js";
import { isResourceTrackingEnabled } from "../engine_assetdatabase.js";
import { Context } from "../engine_setup.js";
import { type ConstructorConcrete, GLTF, type SourceIdentifier } from "../engine_types.js";
import { getParam } from "../engine_utils.js";
import { NEEDLE_lightmaps } from "../extensions/NEEDLE_lightmaps.js";
import { EXT_texture_exr } from "./EXT_texture_exr.js";
import { NEEDLE_components } from "./NEEDLE_components.js";
import { NEEDLE_gameobject_data } from "./NEEDLE_gameobject_data.js";
import { NEEDLE_lighting_settings } from "./NEEDLE_lighting_settings.js";
import { NEEDLE_materialx } from "./NEEDLE_materialx.js";
import { NEEDLE_persistent_assets } from "./NEEDLE_persistent_assets.js";
import { NEEDLE_pmrem } from "./NEEDLE_pmrem.js";
import { NEEDLE_progressive } from "./NEEDLE_progressive.js";
import { NEEDLE_render_objects } from "./NEEDLE_render_objects.js";
import { NEEDLE_techniques_webgl } from "./NEEDLE_techniques_webgl.js";
import { InternalUsageTrackerPlugin } from "./usage_tracker.js";

const debug = getParam("debugextensions");

// lazily import the GLTFAnimationPointerExtension in case it doesnt exist (e.g. using vanilla three)
let GLTFAnimationPointerExtension: any;
const KHR_ANIMATIONPOINTER_IMPORT = import("@needle-tools/three-animation-pointer").then(async mod => {
    GLTFAnimationPointerExtension = mod.GLTFAnimationPointerExtension;
    return GLTFAnimationPointerExtension;
}).catch(e => {
    console.warn("Failed to import GLTFLoaderAnimationPointer. Please use @needle-tools/three-animationpointer for full KHR_animation support", e);
});


/**
 * Callback type for glTF import plugins. See {@link INeedleGLTFExtensionPlugin.onImport}
 */
export type OnImportCallback = (loader: GLTFLoader, url: string, context: Context) => void;

/**
 * Callback type for glTF export plugins. See {@link INeedleGLTFExtensionPlugin.onExport}
 */
export type OnExportCallback = (exporter: GLTFExporter, context: Context) => void;

/**
 * Interface for registering custom glTF extensions to the Needle Engine GLTFLoaders.   
 * Register your plugin using the {@link addCustomExtensionPlugin} method
 */
export interface INeedleGLTFExtensionPlugin {
    /** The Name of your plugin */
    name: string;
    /** Called before starting to load a glTF file. This callback can be used to add custom extensions to the [GLTFLoader](https://threejs.org/docs/#GLTFLoader.register)  
     * 
     * @example Add a custom extension to the GLTFloader
     * ```ts
     * onImport: (loader, url, context) => {
     *    loader.register((parser) => new MyCustomExtension(parser));
     * }
     * ```
     */
    onImport?: OnImportCallback;
    /** Called after a glTF file has been loaded */
    onLoaded?: (url: string, gltf: GLTF, context: Context) => void;
    /** Called before starting to export a glTF file. This callback can be used to add custom extensions to the [GLTFExporter](https://threejs.org/docs/#examples/en/exporters/GLTFExporter.register) 
     * 
     * @example Add a custom extension to the GLTFExporter
     * ```ts
     * onExport: (exporter, context) => {
     *    exporter.register((writer) => new MyCustomExportExtension(writer));
     * }
     * 
    */
    onExport?: OnExportCallback;
}

/**
 * Registered custom glTF extension plugins
 */
const _plugins = new Array<INeedleGLTFExtensionPlugin>();

/** Register callbacks for registering custom gltf importer or exporter plugins */
export function addCustomExtensionPlugin(ext: INeedleGLTFExtensionPlugin) {
    if (!_plugins.includes(ext)) {
        _plugins.push(ext);
    }
}
/** Unregister callbacks for registering custom gltf importer or exporter plugins */
export function removeCustomImportExtensionType(ext: INeedleGLTFExtensionPlugin) {
    const index = _plugins.indexOf(ext);
    if (index >= 0)
        _plugins.splice(index, 1);
}


/** Registers the Needle Engine components extension */
export function registerComponentExtension(loader: GLTFLoader | Loader | object): NEEDLE_components | null {
    if (loader instanceof GLTFLoader) {
        const ext = new NEEDLE_components();
        loader.register(p => {
            ext.parser = p;
            return ext;
        });
        return ext;
    }

    return null;
}



class PointerResolver {
    resolvePath(path: string) {
        if (path.includes('/extensions/builtin_components/'))
            return path.replace('/extensions/builtin_components/', '/userData/components/');
        if (path.includes('extensions/builtin_components/'))
            return path.replace('extensions/builtin_components/', '/userData/components/');
        return path;
    }
}

export async function registerExtensions(loader: GLTFLoader, context: Context, url: string, sourceId: SourceIdentifier) {

    // Make sure to remove any url parameters from the sourceId (because the source id in the renderer does not have a ?v=xxx so it will not be able to register the resolved lightmap otherwise)
    const idEnd = url.indexOf("?");
    if (idEnd >= 0) url = url.substring(0, idEnd);
    if (!sourceId) {
        sourceId = url;
    }
    if (sourceId.startsWith("blob:") || sourceId.startsWith("data:")) {
        console.debug("[GLTFLoader] Suspicious sourceId detected");
    }

    loader.register(p => new NEEDLE_gameobject_data(p));
    loader.register(p => new NEEDLE_persistent_assets(p));
    loader.register(p => new NEEDLE_lightmaps(p, context.lightmaps, sourceId));
    loader.register(p => new NEEDLE_lighting_settings(p, sourceId, context));
    loader.register(p => new NEEDLE_techniques_webgl(p, sourceId));
    loader.register(p => new NEEDLE_render_objects(p, sourceId));
    loader.register(p => new NEEDLE_progressive(p));
    loader.register(p => new EXT_texture_exr(p));
    loader.register(p => new NEEDLE_pmrem(p));
    loader.register(p => new NEEDLE_materialx(context, loader, url, p));
    if (isResourceTrackingEnabled()) loader.register(p => new InternalUsageTrackerPlugin(p))

    await KHR_ANIMATIONPOINTER_IMPORT.catch(_ => { })
    loader.register(p => {
        if (GLTFAnimationPointerExtension) {
            const ext = new GLTFAnimationPointerExtension(p);
            const setPointerResolverFunction = ext.setAnimationPointerResolver;
            setPointerResolverFunction.bind(ext)(new PointerResolver());
            return ext;
        }
        else {
            if (debug || isDevEnvironment()) console.error("Missing KHR_animation_pointer extension...")
            return {
                name: "KHR_animation_pointer_NOT_AVAILABLE"
            };
        }
    });

    for (const plugin of _plugins) {
        if (plugin.onImport) plugin.onImport(loader, url, context);
    }
}

export function registerExportExtensions(exp: GLTFExporter, context: Context) {
    for (const ext of _plugins)
        if (ext.onExport) ext.onExport(exp, context);
}

/** @internal */
export function invokeLoadedImportPluginHooks(url: string, gltf: GLTF, context: Context) {
    for (const ext of _plugins)
        if (ext.onLoaded) ext.onLoaded(url, gltf, context);
}