import { Material, MeshStandardMaterial, DoubleSide, FrontSide, MaterialParameters } from "three";
import { GLTFLoader, GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
import { ready, state, MaterialXContext } from "../materialx.js";
import { debug } from "../utils.js";
import { MaterialXMaterial } from "../materialx.material.js";
import { Callbacks as callbacks } from "../materialx.helper.js";

// TypeScript interfaces matching the C# data structures
export type MaterialX_root_extension = {
    /** e.g. 1.39 */
    version: string;
    /** e.g. "Material" */
    name: string;
    /** MaterialX xml content */
    mtlx: string;
    /** MaterialX texture pointers */
    textures: Array<{ name: string, pointer: string }>;
    shaders?: Array<{
        /** The materialx node name */
        name: string,
        /** The original name of the shader */
        originalName: string,
    }>,
}

export type MaterialX_material_extension = {
    /** The MaterialX material name */
    name: string;
    /** The index of the shader in the shaders array of the root extension. */
    shader?: number;
}

type MaterialDefinition = {
    name?: string; // Optional name for the material
    doubleSided?: boolean; // Whether the material is double-sided
    extensions?: {
        [key: string]: any; // Extensions for the material, including MaterialX
    },
}

// init.context.getRenderer().capabilities.getMaxPrecision("highp")
export type MaterialXLoaderOptions = {
    /** The URL of the GLTF file being loaded */
    cacheKey?: string;
    /** Parameters for the MaterialX loader */
    parameters?: Pick<MaterialParameters, "precision">;
}

// MaterialX loader extension for js GLTFLoader
export class MaterialXLoader implements GLTFLoaderPlugin {
    readonly name = "NEEDLE_materials_mtlx";

    private readonly _generatedMaterials: MaterialXMaterial[] = [];

    private _documentReadyPromise: Promise<any> | null = null;

    get materialX_root_data() {
        return this.parser.json.extensions?.[this.name] as MaterialX_root_extension | null;
    }

    /** Generated materialX materials */
    get materials() {
        return this._generatedMaterials;
    }

    /**
     * MaterialXLoader constructor
     * @param parser The GLTFParser instance
     * @param cacheKey The URL of the GLTF file
     * @param context The context for the GLTF loading process
     */
    constructor(
        private parser: GLTFParser,
        private options: MaterialXLoaderOptions,
        private context: MaterialXContext
    ) {
        if (debug) console.log("MaterialXLoader created for parser");
        // Start loading of MaterialX environment if the root extension exists
        if (this.materialX_root_data) {
            ready();
        }
    }


    loadMaterial(materialIndex: number): Promise<Material> | null {
        const materialDef = this.parser.json.materials?.[materialIndex];
        if (!materialDef?.extensions?.[this.name]) {
            return null;
        }
        // Wrap the async implementation
        return this._loadMaterialAsync(materialIndex);
    }


    private async _loadMaterialAsync(materialIndex: number): Promise<Material> {

        const materialDef = this.parser.json.materials?.[materialIndex] as MaterialDefinition;
        if (debug) console.log("[MaterialX] extension found in material:", materialDef.extensions?.[this.name]);

        // Handle different types of MaterialX data
        const ext = materialDef.extensions?.[this.name] as MaterialX_material_extension;

        const mtlx = this.materialX_root_data?.mtlx;

        if (ext && mtlx) {

            const materialOptions: MaterialXMaterialOptions = {
                ...this.options,
            }

            if (!materialOptions.parameters) materialOptions.parameters = {};

            if (materialOptions.parameters?.side === undefined && materialDef.doubleSided !== undefined) {
                materialOptions.parameters.side = materialDef.doubleSided ? DoubleSide : FrontSide;
            }

            return createMaterialXMaterial(mtlx, ext.name, {
                cacheKey: this.options.cacheKey || "",
                getTexture: async url => {
                    // Find the index of the texture in the parser
                    const filenameWithoutExt = url.split('/').pop()?.split('.').shift() || '';

                    // Resolve the texture from the MaterialX root extension
                    if (this.materialX_root_data) {
                        const textures = this.materialX_root_data.textures || [];
                        let index = -1;
                        for (const texture of textures) {
                            // Find the texture by name and use the pointer string to get the index
                            if (texture.name === filenameWithoutExt) {
                                const ptr = texture.pointer;
                                const indexStr = ptr.substring("/textures/".length);
                                index = parseInt(indexStr);

                                if (isNaN(index) || index < 0) {
                                    console.error("[MaterialX] Invalid texture index in pointer:", ptr);
                                    return;
                                }
                                else {
                                    if (debug) console.log("[MaterialX] Texture index found:", index, "for", filenameWithoutExt);
                                }
                            }
                        }

                        if (index < 0) {
                            console.error("[MaterialX] Texture not found in parser:", filenameWithoutExt, this.parser.json);
                            return;
                        }
                        return this.parser.getDependency("texture", index);
                    }
                    return null;
                }
            }, materialOptions, this.context)
                // Cache and return the generated material
                .then(mat => {
                    if (mat instanceof MaterialXMaterial) this._generatedMaterials.push(mat);
                    return mat;
                })
        }

        // Return fallback material instead of null
        const fallbackMaterial = new MeshStandardMaterial();
        fallbackMaterial.name = "MaterialX_Fallback";
        return fallbackMaterial;
    }

}


type MaterialXMaterialOptions = {
    parameters?: MaterialParameters;
}

/**
 * Add the MaterialXLoader to the GLTFLoader instance.
 */
export function useNeedleMaterialX(loader: GLTFLoader, options?: MaterialXLoaderOptions, context?: MaterialXContext) {
    loader.register(p => {
        const loader = new MaterialXLoader(p, options || {}, context || {});
        return loader;
    });
}




// Parse the MaterialX document once and cache it
async function load(mtlx: string): Promise<any> {
    // Ensure MaterialX is initialized
    await ready();
    if (!state.materialXModule) {
        throw new Error("[MaterialX] module failed to initialize");
    }
    // Create MaterialX document and parse ALL the XML data from root
    const doc = state.materialXModule.createDocument();
    doc.setDataLibrary(state.materialXStdLib);
    // Parse all MaterialX XML strings from the root data
    await state.materialXModule.readFromXmlString(doc, mtlx, "");
    if (debug) console.log("[MaterialX] root document parsed successfully");
    return doc;
}

export async function createMaterialXMaterial(mtlx: string, materialNodeName: string, loaders: callbacks, options?: MaterialXLoaderOptions, context?: MaterialXContext): Promise<Material> {
    try {
        if (debug) console.log(`Creating MaterialX material: ${materialNodeName}`);

        const doc = await load(mtlx);

        if (!state.materialXModule || !state.materialXGenerator || !state.materialXGenContext) {
            console.warn("[MaterialX] WASM module not ready, returning fallback material");
            const fallbackMaterial = new MeshStandardMaterial();
            fallbackMaterial.name = `MaterialX_Fallback_${materialNodeName}`;
            return fallbackMaterial;
        }

        // Find the renderable element following MaterialX example pattern exactly
        let renderableElement: any = null;
        let foundRenderable = false;

        if (debug) console.log("[MaterialX] document", doc);

        // Search for material nodes first (following the reference pattern)
        const materialNodes = doc.getMaterialNodes();
        if (debug) console.log(`[MaterialX] Found ${materialNodes.length} material nodes in document`, materialNodes);

        // Handle both array and vector-like APIs
        for (let i = 0; i < materialNodes.length; ++i) {
            const materialNode = materialNodes[i];
            if (materialNode) {
                const name = materialNode.getNamePath();
                if (debug) console.log(`[MaterialX] Scan material[${i}]: ${name}`);

                // Find the matching material
                if (materialNodes.length === 1 || name == materialNodeName) {
                    materialNodeName = name;
                    renderableElement = materialNode;
                    foundRenderable = true;
                    if (debug) console.log(`[MaterialX] Use material node: '${name}'`);
                    break;
                }
            }
        }

        /*
        // If no material nodes found, search nodeGraphs
        if (!foundRenderable) {
            const nodeGraphs = doc.getNodeGraphs();
            console.log(`Found ${nodeGraphs.length} node graphs in document`);
            const nodeGraphsLength = nodeGraphs.length;
            for (let i = 0; i < nodeGraphsLength; ++i) {
                const nodeGraph = nodeGraphs[i];
                if (nodeGraph) {
                    // Skip any nodegraph that has nodedef or sourceUri
                    if ((nodeGraph as any).hasAttribute('nodedef') || (nodeGraph as any).hasSourceUri()) {
                        continue;
                    }
                    // Skip any nodegraph that is connected to something downstream
                    if ((nodeGraph as any).getDownstreamPorts().length > 0) {
                        continue;
                    }
                    const outputs = (nodeGraph as any).getOutputs();
                    for (let j = 0; j < outputs.length; ++j) {
                        const output = outputs[j];
                        if (output && !foundRenderable) {
                            renderableElement = output;
                            foundRenderable = true;
                            break;
                        }
                    }
                    if (foundRenderable) break;
                }
            }
        }
         
        // If still no element found, search document outputs
        if (!foundRenderable) {
            const outputs = doc.getOutputs();
            console.log(`Found ${outputs.length} output nodes in document`);
            const outputsLength = outputs.length;
            for (let i = 0; i < outputsLength; ++i) {
                const output = outputs[i];
                if (output && !foundRenderable) {
                    renderableElement = output;
                    foundRenderable = true;
                    break;
                }
            }
        }
        */

        if (!renderableElement) {
            console.warn(`[MaterialX] No renderable element found in MaterialX document (${name})`);
            const fallbackMaterial = new MeshStandardMaterial();
            fallbackMaterial.color.set(0xff00ff);
            fallbackMaterial.name = `MaterialX_NoRenderable_${name}`;
            return fallbackMaterial;
        }

        if (debug) console.log("[MaterialX] Using renderable element for shader generation");

        // Check transparency and set context options like the reference
        const isTransparent = state.materialXModule.isTransparentSurface(renderableElement, state.materialXGenerator.getTarget());
        state.materialXGenContext.getOptions().hwTransparency = isTransparent;

        // Generate shaders using the element's name path
        if (debug) console.log("[MaterialX] Generating MaterialX shaders...");
        const elementName = (renderableElement as any).getNamePath ? (renderableElement as any).getNamePath() : (renderableElement as any).getName();

        const shader = state.materialXGenerator.generate(elementName, renderableElement, state.materialXGenContext);

        // const rootExtension = this.materialX_root_data;

        // const shaderInfo = rootExtension && material_extension.shader !== undefined && material_extension.shader >= 0
        //     ? rootExtension.shaders?.[material_extension.shader]
        //     : null;

        const shaderMaterial = new MaterialXMaterial({
            name: materialNodeName,
            shaderName: null, //shaderInfo?.originalName || shaderInfo?.name || null,
            shader,
            context: context || {},
            parameters: {
                transparent: isTransparent,
                ...options?.parameters,
            },
            loaders: loaders,
        });

        // Add debugging to see if the material compiles correctly
        if (debug) console.log("[MaterialX]  material created:", shaderMaterial.name);
        return shaderMaterial;

    } catch (error) {
        // This is a wasm error (an int) that we need to resolve
        console.error(`[MaterialX] Error creating MaterialX material (${name}):`, error);
        // Return a fallback material with stored MaterialX data
        const fallbackMaterial = new MeshStandardMaterial();
        fallbackMaterial.color.set(0xff00ff);
        fallbackMaterial.name = `MaterialX_Error_${name}`;
        return fallbackMaterial;
    }
}