import { Bone, Euler, Object3D, Quaternion, SkinnedMesh, Vector3 } from "three";

import { $shadowDomOwner } from "../engine-components/ui/Symbols.js";
import { type AssetReference } from "./engine_addressables.js";
import { __internalNotifyObjectDestroyed as __internalRemoveReferences, disposeObjectResources } from "./engine_assetdatabase.js";
import { ComponentLifecycleEvents } from "./engine_components_internal.js";
import { activeInHierarchyFieldName } from "./engine_constants.js";
import { editorGuidKeyName } from "./engine_constants.js";
import { $isUsingInstancing, InstancingUtil } from "./engine_instancing.js";
import { processNewScripts } from "./engine_mainloop_utils.js";
import { InstantiateIdProvider } from "./engine_networking_instantiate.js";
import { assign, ISerializable } from "./engine_serialization_core.js";
import { Context, registerComponent } from "./engine_setup.js";
import { getTempQuaternion, logHierarchy, setWorldPosition, setWorldQuaternion } from "./engine_three_utils.js";
import { type Constructor, type GuidsMap, type IComponent as Component, type IComponent, IEventList, type IGameObject as GameObject, type UIDProvider } from "./engine_types.js";
import { deepClone, getParam, tryFindObject } from "./engine_utils.js";
import { apply } from "./js-extensions/index.js";

const debug = getParam("debuggetcomponent");
const debugInstantiate = getParam("debuginstantiate");

export type IInstantiateOptions = {
    idProvider?: UIDProvider;
    //** parent guid or object */
    parent?: string | Object3D;
    /** position in local space. Set `keepWorldPosition` to true if this is world space */
    position?: Vector3 | [number, number, number];
    /** for duplicatable parenting */
    keepWorldPosition?: boolean;
    /** rotation in local space. Set `keepWorldPosition` to true if this is world space */
    rotation?: Quaternion | Euler | [number, number, number];
    scale?: Vector3 | [number, number, number];
    /** if the instantiated object should be visible */
    visible?: boolean;
    context?: Context;
    /** If true the components will be cloned as well
     * @default true
     */
    components?: boolean;
}

/**
 * Instantiation options for {@link syncInstantiate}
 */
export class InstantiateOptions implements IInstantiateOptions {
    idProvider?: UIDProvider | undefined;
    parent?: string | undefined | Object3D;
    keepWorldPosition?: boolean
    position?: Vector3 | [number, number, number] | undefined;
    rotation?: Quaternion | Euler | [number, number, number] | undefined;
    scale?: Vector3 | [number, number, number] | undefined;
    visible?: boolean | undefined;
    context?: Context | undefined;
    components?: boolean | undefined;

    clone() {
        const clone = new InstantiateOptions();
        clone.idProvider = this.idProvider;
        clone.parent = this.parent;
        clone.keepWorldPosition = this.keepWorldPosition;
        clone.position = Array.isArray(this.position) ? [...this.position] : this.position?.clone();
        clone.rotation = Array.isArray(this.rotation) ? [...this.rotation] : this.rotation?.clone();
        clone.scale = Array.isArray(this.scale) ? [...this.scale] : this.scale?.clone();
        clone.visible = this.visible;
        clone.context = this.context;
        clone.components = this.components;
        return clone;
    }

    /** Copy fields from another object, clone field references */
    cloneAssign(other: InstantiateOptions | IInstantiateOptions) {
        this.idProvider = other.idProvider;
        this.parent = other.parent;
        this.keepWorldPosition = other.keepWorldPosition;
        this.position = Array.isArray(other.position) ? [...other.position] : other.position?.clone();
        this.rotation = Array.isArray(other.rotation) ? [...other.rotation] : other.rotation?.clone();
        this.scale = Array.isArray(other.scale) ? [...other.scale] : other.scale?.clone();
        this.visible = other.visible;
        this.context = other.context;
        this.components = other.components;
    }
}


// export function setActive(go: Object3D, active: boolean, processStart: boolean = true) {
//     if (!go) return;
//     go.visible = active;
//     main.updateActiveInHierarchyWithoutEventCall(go);
//     if (active && processStart)
//         main.processStart(Context.Current, go);
// }


// Object.defineProperty(Object3D.prototype, "visible", {
//     get: function () {
//         return this._visible;
//     },
//     set: function (val) {
//         // const changed = val !== this._visible;
//         this._visible = val;
//         // if (changed) {
//         //     setActive(this, val);
//         // }
//     }
// });


export function isActiveSelf(go: Object3D): boolean {
    return go.visible;
}

export function setActive(go: Object3D, active: boolean | number): boolean {
    if (typeof active === "number") active = active > .5;
    // go[$isActive] = active;
    go.visible = active;
    return go.visible;// go[$isActive];
}

export function isActiveInHierarchy(go: Object3D): boolean {
    return go[activeInHierarchyFieldName] || isUsingInstancing(go);
}

export function markAsInstancedRendered(go: Object3D, instanced: boolean) {
    go[$isUsingInstancing] = instanced;
}

export function isUsingInstancing(instance: Object3D): boolean { return InstancingUtil.isUsingInstancing(instance); }


export function findByGuid(guid: string, hierarchy: Object3D): GameObject | IComponent | null | undefined {
    return tryFindObject(guid, hierarchy, true, true);
}


const $isDestroyed = Symbol("isDestroyed");
export function isDestroyed(go: Object3D): boolean {
    return go[$isDestroyed];
}
export function setDestroyed(go: Object3D, value: boolean) {
    go[$isDestroyed] = value;
}

const $isDontDestroy = Symbol("isDontDestroy");

/** Mark an Object3D or component as not destroyable
 * @param instance the object to be marked as not destroyable
 * @param value true if the object should not be destroyed in `destroy`
 */
export function setDontDestroy(instance: Object3D | Component, value: boolean = true) {
    instance[$isDontDestroy] = value;
}

const destroyed_components: Array<IComponent> = [];
const destroyed_objects: Array<Object3D> = [];

/**
 * Destroys a GameObject or Component, removing it from the scene and cleaning up resources.  
 * Calls `onDisable()` and `onDestroy()` lifecycle methods on all affected components.  
 *
 * @param instance The Object3D or Component to destroy
 * @param recursive If true (default), also destroys all children recursively
 * @param dispose If true, also disposes GPU resources (geometries, materials, textures)
 *
 * @example Destroy an object
 * ```ts
 * import { destroy } from "@needle-tools/engine";
 * destroy(this.gameObject);
 * ```
 *
 * @example Destroy with resource disposal
 * ```ts
 * destroy(myObject, true, true); // recursive + dispose GPU resources
 * ```
 *
 * @see {@link GameObject.destroy} for the static method equivalent
 * @see {@link setDontDestroy} to mark objects as non-destroyable
 */
export function destroy(instance: Object3D | Component, recursive: boolean = true, dispose: boolean = false) {
    destroyed_components.length = 0;
    destroyed_objects.length = 0;
    internalDestroy(instance, recursive, true);
    for (const comp of destroyed_components) {
        comp.gameObject = null!;
        //@ts-ignore
        comp.context = null;
    }
    // dipose resources and remove references
    for (const obj of destroyed_objects) {
        setDestroyed(obj, true);
        if (dispose) {
            disposeObjectResources(obj);
            // This needs to be called after disposing because it removes the references to resources
            __internalRemoveReferences(obj);
        }
    }
    destroyed_objects.length = 0;
    destroyed_components.length = 0;
}

function internalDestroy(instance: Object3D | Component, recursive: boolean = true, isRoot: boolean = true) {
    if (instance === null || instance === undefined)
        return;

    const comp = instance as Component;
    if (comp.isComponent) {
        // Handle Component
        if (comp[$isDontDestroy]) return;
        destroyed_components.push(comp);
        const go = comp.gameObject;
        comp.__internalDisable();
        comp.__internalDestroy();
        comp.gameObject = go;
        return;
    }

    // handle Object3D
    if (instance[$isDontDestroy]) return;

    const obj = instance as GameObject;
    if (debug) console.log(obj);
    destroyed_objects.push(obj);

    // first disable and call onDestroy on components
    const components = obj.userData?.components;
    if (components != null && Array.isArray(components)) {
        let lastLength = components.length;
        for (let i = 0; i < components.length; i++) {
            const comp: Component = components[i];
            internalDestroy(comp, recursive, false);
            // components will be removed from componentlist in destroy
            if (components.length < lastLength) {
                lastLength = components.length;
                i--;
            }
        }
    }
    // then continue in children of the passed in object
    if (recursive && obj.children) {
        for (const ch of obj.children) {
            internalDestroy(ch, recursive, false);
        }
    }

    if (isRoot)
        obj.removeFromParent();
}

declare type ForEachComponentCallback = (comp: Component) => any;

/**
 * Iterates over all components on an Object3D and optionally its children.
 * The callback can return a value to stop iteration early.
 *
 * @param instance The Object3D to iterate components on
 * @param cb Callback function called for each component. Return a value to stop iteration.
 * @param recursive If true (default), also iterates components on all children
 * @returns The first non-undefined value returned by the callback, or undefined
 *
 * @example Find first Rigidbody in hierarchy
 * ```ts
 * const rb = foreachComponent(myObject, comp => {
 *   if (comp instanceof Rigidbody) return comp;
 * });
 * ```
 */
export function foreachComponent(instance: Object3D, cb: ForEachComponentCallback, recursive: boolean = true): any {
    return internalForEachComponent(instance, cb, recursive);
}

export function* foreachComponentEnumerator<T extends IComponent>(instance: Object3D, type?: Constructor<T>, includeChildren: boolean = false, maxLevel: number = 999, _currentLevel: number = 0): Generator<T> {
    if (!instance?.userData.components) return;
    if (_currentLevel > maxLevel) return;
    for (const comp of instance.userData.components) {
        if (type && comp?.isComponent === true && comp instanceof type) {
            yield comp;
        }
        else {
            yield comp;
        }
    }
    if (includeChildren === true) {
        for (const ch of instance.children) {
            yield* foreachComponentEnumerator(ch, type, true, maxLevel, _currentLevel + 1);
        }
    }
}

function internalForEachComponent(instance: Object3D, cb: ForEachComponentCallback, recursive: boolean, level: number = 0): any {
    if (!instance) return;
    if (!instance.isObject3D) {
        new Error("Expected Object3D but got " + instance);
    }
    if (level > 1000) {
        console.warn("Failed to iterate components: too many levels");
        return;
    }
    if (instance.userData?.components) {
        for (let i = 0; i < instance.userData.components.length; i++) {
            const comp = instance.userData.components[i];
            if (comp?.isComponent === true) {
                const res = cb(comp);
                if (res !== undefined) return res;
            }
        }
    }

    if (recursive && instance.children) {
        // childArrayBuffer.length = 0;
        // childArrayBuffer.push(...instance.children);
        const nextLevel = level + 1;
        for (let i = 0; i < instance.children.length; i++) {
            const child = instance.children[i];
            if (!child) continue;
            const res = internalForEachComponent(child, cb, recursive, nextLevel);
            if (res !== undefined) return res;
        }
        // childArrayBuffer.length = 0;
    }

}

declare type ObjectCloneReference = {
    readonly original: object;
    readonly clone: object;
}


declare type InstantiateReferenceMap = Record<string, ObjectCloneReference>;
declare type NewObjectReferenceMap = Record<string, { target: object, key: string }>;

/**
 * Provides access to the instantiated object and its clone
 */
export declare type InstantiateContext = Readonly<InstantiateReferenceMap>;

/**
 * Creates a copy (clone) of a GameObject or loads and instantiates an AssetReference.
 * All components on the original object are cloned and `awake()` is called on them.
 *
 * @param instance The Object3D to clone, or an AssetReference to load and instantiate
 * @param opts Optional instantiation settings (position, rotation, scale, parent)
 * @returns The cloned GameObject, or a Promise<Object3D> if instantiating from AssetReference
 *
 * @example Clone an object
 * ```ts
 * import { instantiate } from "@needle-tools/engine";
 * const clone = instantiate(original);
 * clone.position.set(1, 0, 0);
 * this.context.scene.add(clone);
 * ```
 *
 * @example Instantiate with options
 * ```ts
 * const clone = instantiate(original, {
 *   parent: parentObject,
 *   position: new Vector3(0, 1, 0),
 *   rotation: new Quaternion()
 * });
 * ```
 *
 * @example Instantiate from AssetReference
 * ```ts
 * const instance = await instantiate(myAssetRef);
 * if (instance) this.context.scene.add(instance);
 * ```
 *
 * @see {@link GameObject.instantiate} for the static method equivalent
 * @see {@link destroy} to remove instantiated objects
 */
export function instantiate(instance: AssetReference, opts?: IInstantiateOptions | null): Promise<Object3D | null>
export function instantiate(instance: GameObject | Object3D, opts?: IInstantiateOptions | null): GameObject
export function instantiate(instance: AssetReference | GameObject | Object3D, opts?: IInstantiateOptions | null | undefined): GameObject | Promise<Object3D | null> {

    if ("isAssetReference" in instance) {
        return instance.instantiate(opts ?? undefined);
    }

    let options: InstantiateOptions | null = null;

    if (opts !== null && opts !== undefined) {
        // if x is defined assume this is a vec3 - this is just to not break everything at once and stay a little bit backwards compatible
        if (opts["x"] !== undefined) {
            options = new InstantiateOptions();
            options.position = opts as unknown as Vector3;
        }
        else {
            // if (opts instanceof InstantiateOptions)
            options = opts as InstantiateOptions;
        }
    }

    let context = Context.Current;
    if (options?.context) context = options.context;
    if (debug && context.alias)
        console.log("context", context.alias);

    // we need to create the id provider before calling internal instantiate because cloned gameobjects also create new guids
    if (options && !options.idProvider) {
        options.idProvider = new InstantiateIdProvider(Date.now());
    }

    const components: Array<Component> = [];
    const referencemap: InstantiateReferenceMap = {}; // used to resolve references on components to components on other gameobjects to their new counterpart
    const skinnedMeshes: InstantiateReferenceMap = {}; // used to resolve skinned mesh bones
    const clone = internalInstantiate(context, instance, options, components, referencemap, skinnedMeshes);

    if (clone) {
        resolveReferences(clone, referencemap);
        resolveAndBindSkinnedMeshBones(skinnedMeshes, referencemap);
    }

    if (debug) {
        logHierarchy(instance, true);
        logHierarchy(clone, true);
    }

    const guidsMap: GuidsMap = {};
    if (options?.components !== false) {
        for (const i in components) {
            const copy = components[i];
            const oldGuid = copy.guid;
            if (options && options.idProvider) {
                copy.guid = options.idProvider.generateUUID();
                guidsMap[oldGuid] = copy.guid;
                if (debug)
                    console.log(copy.name, copy.guid)
            }
            registerComponent(copy, context);
            if (copy.__internalNewInstanceCreated)
                copy.__internalNewInstanceCreated();
        }
        for (const i in components) {
            const copy = components[i];
            if (copy.resolveGuids)
                copy.resolveGuids(guidsMap);
            if (copy.enabled === false) continue;
            else copy.enabled = true;
        }

        processNewScripts(context);
    }

    return clone as GameObject;
}


function internalInstantiate(
    context: Context, instance: GameObject | Object3D, opts: IInstantiateOptions | InstantiateOptions | null,
    componentsList: Array<Component>,
    objectsMap: InstantiateReferenceMap,
    skinnedMeshesMap: InstantiateReferenceMap
)
    : GameObject | Object3D | null {
    if (!instance) return null;

    // Don't clone UI shadow objects
    if (instance[$shadowDomOwner]) {
        return null;
    }

    // prepare, remove things that dont work out of the box
    // e.g. user data we want to manually clone
    // also children throw errors (e.g. recursive toJson with nested meshes)
    const userData = instance.userData;
    instance.userData = {};
    const children = instance.children;
    instance.children = [];
    const clone: Object3D | GameObject = instance.clone(false);
    apply(clone);
    // if(instance[$originalGuid])
    //     clone[$originalGuid] = instance[$originalGuid];
    instance.userData = userData;
    instance.children = children;

    // make reference from old id to new object
    objectsMap[instance.uuid] = { original: instance, clone: clone };
    if (debugInstantiate) console.log("ADD", instance, clone)

    if (instance.type === "SkinnedMesh") {
        skinnedMeshesMap[instance.uuid] = { original: instance, clone: clone };
    }

    // DO NOT EVER RENAME BECAUSE IT BREAKS / MIGHT BREAK ANIMATIONS
    // clone.name += " (Clone)";

    if (opts?.visible !== undefined)
        clone.visible = opts.visible;

    if (opts?.idProvider) {
        clone.uuid = opts.idProvider.generateUUID();
        const cloneGo: GameObject = clone as GameObject;
        if (cloneGo) cloneGo.guid = clone.uuid;
    }

    if (instance.animations && instance.animations.length > 0) {
        clone.animations = [...instance.animations];
    }

    const parent = instance.parent;
    if (parent) {
        parent.add(clone);
    }

    // POSITION
    if (opts?.position) {
        if (Array.isArray(opts.position)) {
            const vec = new Vector3();
            vec.fromArray(opts.position);
            clone.worldPosition = vec;
        }
        else {
            clone.worldPosition = opts.position;
        }
    }
    else clone.position.copy(instance.position);

    // ROTATION
    if (opts?.rotation) {
        if (opts.rotation instanceof Quaternion)
            clone.worldQuaternion = opts.rotation;
        else if (opts.rotation instanceof Euler)
            clone.worldQuaternion = getTempQuaternion().setFromEuler(opts.rotation);
        else if (Array.isArray(opts.rotation)) {
            const euler = new Euler();
            euler.fromArray(opts.rotation);
            clone.worldQuaternion = getTempQuaternion().setFromEuler(euler);
        }
    }
    else clone.quaternion.copy(instance.quaternion);

    // SCALE
    if (opts?.scale) {
        // TODO MAJOR: replace with worldscale
        // clone.worldScale = opts.scale;
        if (Array.isArray(opts.scale)) {
            const vec = new Vector3();
            vec.fromArray(opts.scale);
            opts.scale = vec;
        }
        else {
            clone.scale.copy(opts.scale);
        }
    }
    else clone.scale.copy(instance.scale);

    if (opts?.parent && opts.parent !== "scene") {
        let requestedParent: Object3D | null = null;
        if (typeof opts.parent === "string") {
            requestedParent = tryFindObject(opts.parent, context.scene, true);
        }
        else {
            requestedParent = opts.parent;
        }
        if (requestedParent) {
            const func = opts.keepWorldPosition === true ? requestedParent.attach : requestedParent.add;
            if (!func) console.error("Invalid parent object", requestedParent, "received when instantiating:", instance);
            else func.call(requestedParent, clone);
        }
        else console.warn("could not find parent:", opts.parent);
    }

    for (const [key, value] of Object.entries(instance.userData)) {
        if (key === "components") continue;
        clone.userData[key] = value;
    }

    if (instance.userData?.components) {
        const components = instance.userData.components;
        const newComponents: Component[] = [];
        clone.userData.components = newComponents;
        for (let i = 0; i < components.length; i++) {
            const comp = components[i];
            const copy = new comp.constructor();
            onAssignComponent(comp, copy, objectsMap);
            // make sure the original guid stays intact
            if (comp[editorGuidKeyName] !== undefined)
                copy[editorGuidKeyName] = comp[editorGuidKeyName];
            newComponents.push(copy);
            copy.gameObject = clone;
            // copy.transform = clone;
            componentsList.push(copy);
            objectsMap[comp.guid] = { original: comp, clone: copy };
            ComponentLifecycleEvents.dispatchComponentLifecycleEvent("component-added", copy);
        }
    }

    // children should just clone the original transform
    if (opts) {
        opts.position = undefined;
        opts.rotation = undefined;
        opts.scale = undefined;
        opts.parent = undefined;
        opts.visible = undefined;
    }
    for (const ch in instance.children) {
        const child = instance.children[ch];
        const newChild = internalInstantiate(context, child as GameObject, opts, componentsList, objectsMap, skinnedMeshesMap);
        if (newChild) {
            objectsMap[newChild.uuid] = { original: child, clone: newChild };
            clone.add(newChild);
        }
    }
    return clone;
}


function onAssignComponent(source: any, target: any, _newObjectsMap: InstantiateReferenceMap) {
    assign(target, source, undefined, {
        // onAssigned: (target, key, _oldValue, value) => {
            // if (value !== null && typeof value === "object") {
            //     const serializable = target as ISerializable;
            //     if (serializable?.$serializedTypes?.[key]) {
            //         if (!(value instanceof Object3D)) {
            //             // let clone = null;
            //             // if ("clone" in value) {
            //             //     if (canClone(value)) clone = (value as any).clone();
            //             // }
            //             // else {
            //             //     clone = Object.assign(Object.create(Object.getPrototypeOf(value)), value);
            //             // }
            //             // if (clone) {
            //             //     console.debug(key, { target, value, clone })
            //             //     target[key] = clone;
            //             //     findNestedReferences(clone, objectsMap);
            //             // }
            //             // else console.debug("Could not clone value for key", key, value);
            //         }
            //         else {
            //             console.log("ASSIGNED", value)
            //         }

            //         recursiveAssign(target, target[key], newObjectsMap);
            //     }

            // }
        // }
    });
}

// function findNestedReferences(object: object, map: InstantiateReferenceMap) {
//     const keys = Object.keys(object);
//     for (const key of keys) {
//         const val = (object as any)[key];
//         if (val instanceof Object3D) {
//             if ("guid" in val && val.guid) {
//                 console.log("FOUND ", val.guid, val)
//                 map[val.guid] = { original: val, clone: null };
//             }
//         }
//         else if (typeof val === "object" && val !== null) {
//             findNestedReferences(val, map);
//         }
//     }
// }

function resolveAndBindSkinnedMeshBones(
    skinnedMeshes: { [key: string]: ObjectCloneReference },
    newObjectsMap: { [key: string]: ObjectCloneReference }
) {
    for (const key in skinnedMeshes) {
        const val = skinnedMeshes[key];
        const original = val.original as SkinnedMesh;
        const originalSkeleton = original.skeleton;
        const clone = val.clone as SkinnedMesh;
        // clone.updateWorldMatrix(true, true);
        if (!originalSkeleton) {
            console.warn("Skinned mesh has no skeleton?", val);
            continue;
        }
        const originalBones = originalSkeleton.bones;
        const clonedSkeleton = clone.skeleton.clone();

        clone.skeleton = clonedSkeleton;
        clone.bindMatrix.clone().copy(original.bindMatrix);
        // console.log(clone.bindMatrix)
        clone.bindMatrixInverse.copy(original.bindMatrixInverse);
        // clone.bindMatrix.multiplyScalar(.025);
        // console.assert(originalSkeleton.uuid !== clonedSkeleton.uuid);
        // console.assert(originalBones.length === clonedSkeleton.bones.length);
        const bones: Array<Bone> = [];
        clonedSkeleton.bones = bones;
        for (let i = 0; i < originalBones.length; i++) {
            const bone = originalBones[i];
            const newBoneInfo = newObjectsMap[bone.uuid];
            const clonedBone = newBoneInfo.clone as Bone;
            // console.log("NEW BONE: ", clonedBone, "BEFORE", newBoneInfo.original);
            bones.push(clonedBone);
        }
        // clone.skeleton = new Skeleton(bones);
        // clone.skeleton.update();
        // clone.pose();
        // clone.scale.set(1,1,1);
        // clone.position.y += .1;
        // console.log("ORIG", original, "CLONE", clone);
    }
    for (const key in skinnedMeshes) {
        const clone = skinnedMeshes[key].clone as SkinnedMesh;
        clone.skeleton.update();
        // clone.skeleton.calculateInverses();
        clone.bind(clone.skeleton, clone.bindMatrix);
        clone.updateMatrixWorld(true);
        // clone.pose();
    }
}

// private static bindNewSkinnedMeshBones(source, clone) {
//     const sourceLookup = new Map();
//     const cloneLookup = new Map();
//     // const clone = source.clone(false);

//     function parallelTraverse(a, b, callback) {
//         callback(a, b);
//         for (let i = 0; i < a.children.length; i++) {
//             parallelTraverse(a.children[i], b.children[i], callback);
//         }
//     }
//     parallelTraverse(source, clone, function (sourceNode, clonedNode) {
//         sourceLookup.set(clonedNode, sourceNode);
//         cloneLookup.set(sourceNode, clonedNode);
//     });

//     clone.traverse(function (node) {
//         if (!node.isSkinnedMesh) return;
//         const clonedMesh = node;
//         const sourceMesh = sourceLookup.get(node);
//         const sourceBones = sourceMesh.skeleton.bones;

//         clonedMesh.skeleton = sourceMesh.skeleton.clone();
//         clonedMesh.bindMatrix.copy(sourceMesh.bindMatrix);

//         clonedMesh.skeleton.bones = sourceBones.map(function (bone) {
//             return cloneLookup.get(bone);
//         });
//         clonedMesh.bind(clonedMesh.skeleton, clonedMesh.bindMatrix);
//     });
//     return clone;

// }

function resolveReferences(_newInstance: Object3D, newObjectsMap: InstantiateReferenceMap) {
    // for every object that is newly created we want to update references to their newly created counterparts
    // e.g. a collider instance referencing a rigidbody instance should be updated so that 
    // the cloned collider does not reference the cloned rigidbody (instead of the original rigidbody)
    for (const key in newObjectsMap) {
        const val = newObjectsMap[key];
        const clone = val.clone as Object3D | null;
        // resolve references
        if (clone?.isObject3D && clone?.userData?.components) {
            for (let i = 0; i < clone.userData.components.length; i++) {
                const copy = clone.userData.components[i];
                // find referenced within a cloned gameobject
                const entries = Object.entries(copy);
                // console.log(copy, entries);
                for (const [key, value] of entries) {
                    if (Array.isArray(value)) {
                        const clonedArray: Array<any> = [];
                        copy[key] = clonedArray;
                        // console.log(copy, key, value, copy[key]);
                        for (let i = 0; i < value.length; i++) {
                            const entry = value[i];
                            // push value types into new array
                            if (typeof entry !== "object") {
                                clonedArray.push(entry);
                                continue;
                            }
                            const res: any = postProcessNewInstance(copy, key, entry, newObjectsMap);
                            if (res !== undefined) {
                                if (debugInstantiate) console.log("Found new instance for", key, entry, "->", res);
                                clonedArray.push(res);
                            }
                            else {
                                if (debugInstantiate) console.warn("Could not find new instance for", key, entry);
                                clonedArray.push(entry);
                            }
                        }
                        // console.log(copy[key])
                    }
                    else if (typeof value === "object") {
                        const res = postProcessNewInstance(copy, key, value as IComponent | Object3D, newObjectsMap);
                        if (res !== undefined) {
                            copy[key] = res;
                        }
                        else {
                            if (debugInstantiate) console.warn("Could not find new instance for", key, value);
                        }
                    }
                }
            }
        }
    }

}

function postProcessNewInstance(copy: Object3D, key: string, value: IComponent | Object3D | any, newObjectsMap: InstantiateReferenceMap) {
    if (value === null || value === undefined) return;
    if ((value as IComponent).isComponent === true) {
        const originalGameObjectReference = value["gameObject"];
        // console.log(key, value, originalGameObjectReference);
        if (originalGameObjectReference) {
            const id = originalGameObjectReference.uuid;
            const newGameObject = newObjectsMap[id]?.clone;
            if (!newGameObject) {
                // reference has not changed!
                if (debugInstantiate)
                    console.log("reference did not change", key, copy, value);
                return;
            }
            const index = originalGameObjectReference.userData.components.indexOf(value);
            if (index >= 0 && (newGameObject as Object3D).isObject3D) {
                if (debugInstantiate)
                    console.log(key, id);
                const found = (newGameObject as Object3D).userData.components[index];
                return found;
            }
            else {
                console.warn("could not find component", key, value);
            }
        }
    } else if ((value as Object3D).isObject3D === true) {
        // console.log(value);
        if (key === "gameObject") return;
        const originalGameObjectReference = value as Object3D;
        if (originalGameObjectReference) {
            const id = originalGameObjectReference.uuid;
            const newGameObject = newObjectsMap[id]?.clone;
            if (newGameObject) {
                if (debugInstantiate)
                    console.log(key, "old", value, "new", newGameObject);
                return newGameObject;
            }
        }
    }
    else {
        // create new instances for some types that we know should usually not be shared and can safely be cloned
        if (value.isVector4 || value.isVector3 || value.isVector2 || value.isQuaternion || value.isEuler) {
            return value.clone();
        }
        else if (value.isColor === true) {
            return value.clone();
        }
        else if ((value as IEventList).isEventList === true) {
            // create a new instance of the object
            const copy = (value as IEventList).__internalOnInstantiate(newObjectsMap);
            return copy;
        }
    }
}

// const canClone = (value: any) => value.isVector4 || value.isVector3 || value.isVector2 || value.isQuaternion || value.isEuler || value.isColor === true;