import { Material, Object3D, Texture } from "three";
import { GLTFParser, GLTFReference } from "three/examples/jsm/loaders/GLTFLoader.js";

import { debugExtension } from "../engine_default_parameters.js";
import { getParam } from "../engine_utils.js";
import { type IExtensionReferenceResolver } from "./extension_resolver.js";

const debug = getParam("debugresolvedependencies");

declare type GLTFParserWithCache = GLTFParser & { cache: Map<string, any> };

declare type DependencyInfo = {
    prefix: string,
    dependencyName: string,
}

const rootExtensionPrefix = ["/extensions/", "extensions/"];
const defaultDependencies = [
    { prefix: "/nodes/", dependencyName: "node" },
    { prefix: "/meshes/", dependencyName: "mesh" },
    { prefix: "/materials/", dependencyName: "material" },
    { prefix: "/textures/", dependencyName: "texture" },
    { prefix: "/animations/", dependencyName: "animation" },

    // legacy support
    { prefix: "nodes/", dependencyName: "node" },
    { prefix: "meshes/", dependencyName: "mesh" },
    { prefix: "materials/", dependencyName: "material" },
    { prefix: "textures/", dependencyName: "texture" },
    { prefix: "animations/", dependencyName: "animation" },
]

export async function resolveReferences(parser: GLTFParser, obj: object | string) {
    if (debug) console.log(parser, obj);
    const arr: Promise<any>[] = [];
    internalResolve(defaultDependencies, parser as GLTFParserWithCache, obj, arr);
    const res = await Promise.all(arr);
    if (typeof obj === "string" && res.length === 1) {
        return res[0];
    }
    return res;
}

/** 
 * Utility method to check if two materials were created from the same glTF material
 */
export function compareAssociation<T extends Material>(obj1: T, obj2: T): boolean {
    if (!obj1 || !obj2) return false;
    if (obj1["needle:identifier"] != undefined && obj2["needle:identifier"] != undefined)
        return obj1["needle:identifier"] === obj2["needle:identifier"];
    return false;
}

/** 
 * Setting 
 * @hidden
 */
export function maskGltfAssociation(obj: Object3D | Material | Texture | GLTFReference, identifier: string) {
    // Mark an object with an identifier to check if two objects were created from the same source
    obj["needle:identifier"] = identifier;
}

function internalResolve(paths: DependencyInfo[], parser: GLTFParserWithCache, obj: object | string, promises: Promise<any>[]) {
    if (typeof obj === "object" && obj !== undefined && obj !== null) {
        for (const key of Object.keys(obj)) {
            const val = obj[key];
            // handle json pointer in string variable
            if (typeof val === "string") {
                const ext = resolveExtension(parser, val);
                if (ext !== null && ext !== undefined) {
                    if (typeof ext.then === "function")
                        promises.push(ext.then(res => obj[key] = res));
                    else obj[key] = ext;
                }
                else {
                    // e.g. prefix = "/materials/";
                    const res = tryResolveDependency(paths, parser, val);
                    if (res) {
                        promises.push(res.then(res => {
                            obj[key] = res;
                            return res;
                        }));
                        continue;
                    }
                }
            }
            // handle json pointers in arrays
            else if (Array.isArray(val)) {
                for (let i = 0; i < val.length; i++) {
                    const entry = val[i];
                    const ext = resolveExtension(parser, entry);
                    if (ext !== null && ext !== undefined) {
                        if (typeof ext.then === "function")
                            promises.push(ext.then(res => val[i] = res));
                        else val[i] = ext;
                        continue;
                    }
                    for (const dep of paths) {
                        const index = tryGetIndex(dep.prefix, entry);
                        if (index >= 0) {
                            if (debug) console.log(dep, index, dep.dependencyName);
                            promises.push(parser.getDependency(dep.dependencyName, index).then(res => val[i] = res));
                            break;
                        }
                    }
                    // recurse
                    if (typeof entry === "object") {
                        internalResolve(paths, parser, entry, promises);
                    }
                }
            }
            // recurse
            else if (typeof val === "object") {
                internalResolve(paths, parser, val, promises);
            }
        }
    }
    else if (typeof obj === "string") {
        const res = tryResolveDependency(paths, parser, obj);
        if (res) promises.push(res);
    }
}


function resolveExtension(parser: GLTFParser, str): Promise<void> | null {
    if (parser && parser.plugins && typeof str === "string") {
        for (const prefix of rootExtensionPrefix) {
            if (str.startsWith(prefix)) {
                let name = str.substring(prefix.length);
                const endIndex = name.indexOf("/");
                if (endIndex >= 0) name = name.substring(0, endIndex);
                const ext = parser.plugins[name] as any as IExtensionReferenceResolver;
                if (debugExtension)
                    console.log(name, ext);
                if (typeof ext?.resolve === "function") {
                    const path = str.substring(prefix.length + name.length + 1);
                    return ext.resolve(parser, path);
                }
                break;
            }
        }
    }
    return null;
}

function tryResolveDependency(paths: DependencyInfo[], parser: GLTFParser & { cache: Map<string, any> }, str: string): Promise<any> | null {
    for (const dep of paths) {
        const index = tryGetIndex(dep.prefix, str);
        if (index >= 0) {
            if (debug) console.warn("GET DEPENDENCY", dep, index, dep.dependencyName);
            return parser.getDependency(dep.dependencyName, index);
        }
    }
    return null;
}

function tryGetIndex(prefix: string, str: string): number {

    if (typeof str === "string" && str.startsWith(prefix)) {
        const part = str.substring(prefix.length);
        const index = Number.parseInt(part);
        if (index >= 0) {
            return index;
        }
    }
    return -1;
}