import { Loader, LoadingManager, Material, Object3D, TextureLoader } from "three";
import { GLTFLoader, GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";

import { ObjectUtils } from "../engine_create_objects.js";
import { MODULES } from "../engine_modules.js";
import { IContext } from "../engine_types.js";


// #region Utils

export namespace MaterialX {


    /**
     * Utility function to load a MaterialX material from a URL. This can be used in your own code to load MaterialX materials outside of the glTF loading process. The URL should point to a MaterialX XML file.
     */
    export async function loadFromUrl(urlOrXML: string,
        opts?: {
            url?: string,
            loadingManager?: LoadingManager,
            materialNameOrIndex?: number | string
        }
    ): Promise<import("three").Material | null> {

        if (!urlOrXML) throw new Error("URL or XML string is required to load a MaterialX material");

        // Ensure the MaterialX module is loaded
        const module = await MODULES.MaterialX.load();

        // Check if the input is an XML string or a URL
        // And fetch the XML content if it's a URL
        const isXmlString = urlOrXML.trimStart().startsWith("<");
        const xml = isXmlString ? urlOrXML : await fetch(urlOrXML).then(r => r.text()).catch(console.error);
        if (!xml) {
            console.warn("Failed to load MaterialX file from url", urlOrXML);
            return null;
        }

        // For relative texture paths we might need to detect the base directory of the material file. 
        // We can only do this if we have a URL (not an XML string) and if the URL is not a data URL. In that case we can use the URL to determine the base path for textures.
        // This can be used by the loader callback to resolve texture paths relative to the material file.
        let dir: string | undefined = undefined;
        if (opts?.url || !isXmlString) {
            const parts = (opts?.url || urlOrXML).split('/');
            parts.pop();
            dir = parts.join('/');
        }

        const textureLoader = new TextureLoader();
        return module.Experimental_API.createMaterialXMaterial(xml, opts?.materialNameOrIndex ?? 0, {
            getTexture: async url => {
                if (!url.startsWith("http") && !url.startsWith("data:") && !url.startsWith("blob:") && !url.startsWith("file:")) {
                    if (dir) {
                        url = dir + "/" + url;
                    }
                }
                return textureLoader.loadAsync(url).catch(e => {
                    console.warn(`Failed to load texture for MaterialX material ${url}`, e);
                });
            }
        }, {
            cacheKey: urlOrXML,
        })
    }
}


// #region Loader

export class MaterialXLoader extends Loader<Object3D | null> {

    loadAsync(url: string, onProgress?: ((event: ProgressEvent<EventTarget>) => void) | undefined): Promise<Object3D> {
        return new Promise((resolve, reject) => {
            this.load(url, resolve, onProgress, reject);
        });
    }

    load(url: string, onLoad: (data: Object3D) => void, onProgress?: ((event: ProgressEvent<EventTarget>) => void) | undefined, onError?: ((err: unknown) => void) | undefined): void {
        onProgress?.({ type: "progress", loaded: 0, total: 0 } as ProgressEvent);

        MaterialX.loadFromUrl(url, {
        }).then(mat => {
            if (mat) {
                onLoad(this.onLoaded(mat));
            }
            else {
                onError?.(new Error("Failed to load MaterialX material from url: " + url));
            }
        });
    }

    private onLoaded(mat: Material): Object3D {
        const shaderball = ObjectUtils.createPrimitive("ShaderBall", { material: mat });
        return shaderball;
    }
}



// #region GLTF Extension

export class NEEDLE_materialx implements GLTFLoaderPlugin {

    get name(): string {
        return "materialx-loading-helper";
    }

    constructor(
        private readonly context: IContext,
        private readonly loader: GLTFLoader,
        private readonly url: string,
        private readonly parser: GLTFParser,
    ) {
    }

    private mtlxLoader?: import("@needle-tools/materialx").MaterialXLoader;

    async beforeRoot() {
        const mtlxExtension = this.parser.json.extensions?.["NEEDLE_materials_mtlx"];
        if (mtlxExtension) {
            const module = await MODULES.MaterialX.load();
            try {
                this.mtlxLoader = new module.MaterialXLoader(this.parser, {
                    cacheKey: `${this.url}:materialx`,
                    parameters: {
                        precision: this.context.renderer?.capabilities.precision as any,
                    }
                }, {
                    getFrame: () => this.context.time.frame,
                    getTime: () => this.context.time.time,
                })
            }
            catch (error) {
                console.error(error);
            }
        }
    }

    loadMaterial(index) {
        if (this.mtlxLoader) return this.mtlxLoader.loadMaterial(index);
        return null;
    }
}