import type { Color, Euler, Matrix2, Matrix3, Matrix4, Object3D, Quaternion, Vector2, Vector3, Vector4 } from "three";

import { InstantiateIdProvider } from "./engine_networking_instantiate.js";
import { isSerializable } from "./engine_serialization_core.js";
import { type GuidsMap, type IComponent, type UIDProvider, isComponent } from "./engine_types.js";
import { getParam } from "./engine_utils.js";

const debug = getParam("debuginstantiate");

// ————————————————————————————————————————————————————————
// Types
// ————————————————————————————————————————————————————————

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

/** Maps uuid/guid → { original, clone } for Object3D and Component instances */
export type InstantiateReferenceMap = Record<string, ObjectCloneReference>;

/**
 * Provides access to the instantiated object map (used by EventList etc.)
 */
export type InstantiateContext = Readonly<InstantiateReferenceMap>;


// ————————————————————————————————————————————————————————
// ID Provider Cache (moved from engine_gltf_builtin_components.ts)
// ————————————————————————————————————————————————————————

/**
 * Cache of id providers per component/object guid.
 * Ensures deterministic guid generation regardless of scene order.
 */
const idProviderCache = new Map<string, InstantiateIdProvider>();

/** Clear the id provider cache (e.g. when reloading a context) */
export function clearIdProviderCache() {
    idProviderCache.clear();
}

// ————————————————————————————————————————————————————————
// Guid Generation (moved from engine_gltf_builtin_components.ts)
// ————————————————————————————————————————————————————————

export const originalComponentNameKey = Symbol("original-component-name");

// #region hierarchy guids
/**
 * Recursively generates new deterministic guids for all objects and components in a hierarchy.
 * Uses the idProviderCache so that the same source guid always produces the same output guid
 * (needed for networking: all clients must agree on the guids of instantiated objects).
 * Populates guidsMap (oldGuid → newGuid) so string references can be remapped afterwards.
 */
export function generateGuidsForHierarchy(
    obj: Object3D,
    idProvider: UIDProvider | null,
    guidsMap: GuidsMap,
): void {
    if (idProvider === null) return;
    if (!obj) return;
    const prev = (obj as any).guid;

    // Use a cached id provider per object to ensure stable guids regardless of hierarchy order
    const idProviderKey = (obj as any).guid;
    if (idProviderKey?.length) {
        if (!idProviderCache.has(idProviderKey)) {
            if (debug) console.log("Creating InstanceIdProvider with key \"" + idProviderKey + "\" for object " + obj.name);
            idProviderCache.set(idProviderKey, new InstantiateIdProvider(idProviderKey));
        }
    }
    const objectIdProvider = idProviderKey && idProviderCache.get(idProviderKey) || idProvider;

    (obj as any).guid = objectIdProvider.generateUUID();
    if (prev && prev !== "invalid")
        guidsMap[prev] = (obj as any).guid;

    if (obj && obj.userData && obj.userData.components) {
        for (const comp of obj.userData.components) {
            if (comp === null) continue;

            const compIdProviderKey = comp.guid;
            if (compIdProviderKey) {
                if (!idProviderCache.has(compIdProviderKey)) {
                    if (debug) console.log("Creating InstanceIdProvider with key \"" + compIdProviderKey + "\" for component " + comp[originalComponentNameKey]);
                    idProviderCache.set(compIdProviderKey, new InstantiateIdProvider(compIdProviderKey));
                }
            }
            else if (debug) console.warn("Can not create IdProvider: component " + comp[originalComponentNameKey] + " has no guid", comp.guid);
            const componentIdProvider = idProviderCache.get(compIdProviderKey) || idProvider;

            const compPrev = comp.guid;
            comp.guid = componentIdProvider.generateUUID();
            if (compPrev && compPrev !== "invalid")
                guidsMap[compPrev] = comp.guid;
        }
    }
    if (obj.children) {
        for (const child of obj.children) {
            generateGuidsForHierarchy(child as Object3D, idProvider, guidsMap);
        }
    }
}

// ————————————————————————————————————————————————————————
// #region reference resolution
// ————————————————————————————————————————————————————————

/**
 * The unified reference resolution function.
 * Iterates all cloned components in the objectMap and remaps their properties
 * to point at cloned counterparts where appropriate.
 *
 * Handles: Component, Object3D, Array, Map, Set, Record/plain objects,
 * EventList, Vector/Color/Quaternion, and @serializable nested objects.
 */
export function resolveInstanceReferences(objectMap: InstantiateReferenceMap): void {
    for (const key in objectMap) {
        const val = objectMap[key];
        const clone = val.clone as Object3D | null;
        if (!clone?.isObject3D || !clone?.userData?.components) continue;

        for (let i = 0; i < clone.userData.components.length; i++) {
            const component = clone.userData.components[i];
            const entries = Object.entries(component);
            for (const [propKey, propValue] of entries) {
                if (propValue === null || propValue === undefined) continue;
                // Skip primitives that can't be remapped, but allow strings for guid resolution
                if (typeof propValue !== "object" && typeof propValue !== "string") continue;
                const resolved = resolveValue(propKey, propValue, objectMap);
                if (resolved !== undefined) {
                    component[propKey] = resolved;
                }
            }
        }
    }
}

/**
 * Resolves string-based guid references in all components of a hierarchy using a GuidsMap.
 * Used by the glTF loading path where objects get new guids assigned and string references
 * (e.g. PlayableDirector track.outputs) need to be updated.
 */
export function resolveStringGuidsInHierarchy(root: Object3D, guidsMap: GuidsMap): void {
    resolveStringGuidsRecursive(root, guidsMap);
}

function resolveStringGuidsRecursive(obj: Object3D, guidsMap: GuidsMap): void {
    if (obj.userData?.components) {
        for (const component of obj.userData.components) {
            if (component === null) continue;
            resolveStringGuidsInObject(component, guidsMap);
        }
    }
    if (obj.children) {
        for (const child of obj.children) {
            resolveStringGuidsRecursive(child as Object3D, guidsMap);
        }
    }
}

function resolveStringGuidsInObject(obj: any, guidsMap: GuidsMap, visited?: WeakSet<object>): void {
    if (!visited) visited = new WeakSet();
    if (visited.has(obj)) return;
    visited.add(obj);

    for (const key of Object.keys(obj)) {
        const value = obj[key];
        if (value === null || value === undefined) continue;
        if (typeof value === "string") {
            if (guidsMap[value]) {
                obj[key] = guidsMap[value];
            }
        }
        else if (Array.isArray(value)) {
            for (let i = 0; i < value.length; i++) {
                if (typeof value[i] === "string" && guidsMap[value[i]]) {
                    value[i] = guidsMap[value[i]];
                }
                else if (typeof value[i] === "object" && value[i] !== null) {
                    resolveStringGuidsInObject(value[i], guidsMap, visited);
                }
            }
        }
        else if (typeof value === "object") {
            // Skip known non-data objects
            if (value.isObject3D || value.isComponent) continue;
            resolveStringGuidsInObject(value, guidsMap, visited);
        }
    }
}

// #region resolveValue
/**
 * Resolve a single value, returning the remapped value or undefined if no remap needed.
 * This is the core remapping logic called recursively for nested structures.
 */
export function resolveValue(key: string, value: unknown, objectMap: InstantiateReferenceMap): any | undefined {

    // Handle null/undefined early to avoid unnecessary processing
    if (value === undefined) return undefined;
    if (value === null) return null;

    // String guid resolution: if this string is a known guid/uuid in the objectMap,
    // resolve it directly to the clone object. This handles e.g. PlayableDirector track.outputs.
    if (typeof value === "string") {
        const ref = objectMap[value];
        if (ref) {
            return ref.clone;
        }
        return undefined;
    }

    // Primitives: no remapping needed
    if (typeof value !== "object") return undefined;

    // 1. Component → find cloned counterpart by gameObject.uuid + component index
    if (isComponent(value)) {
        return resolveComponentReference(value, objectMap);
    }

    // 2. Object3D → uuid lookup, return clone if found (otherwise external, keep as-is)
    if ((value as Object3D).isObject3D === true) {
        if (key === "gameObject") return undefined;
        const id = (value as Object3D).uuid;
        const cloneRef = objectMap[id]?.clone;
        if (cloneRef) {
            if (debug) console.log(key, "old", value, "new", cloneRef);
            return cloneRef;
        }
        return undefined;
    }

    // 3. Cloneable value types (Vector3, Quaternion, Euler, Color)
    if (isCloneableValueType(value)) {
        return value.clone();
    }

    // 4. Array → create new array, recursively resolve each element
    if (Array.isArray(value)) {
        return resolveArray(key, value, objectMap);
    }

    // 5. Map → create new Map, resolve keys and values
    if (value instanceof Map) {
        return resolveMap(value, objectMap);
    }

    // 6. Set → create new Set, resolve values
    if (value instanceof Set) {
        return resolveSet(value, objectMap);
    }

    // 7. WeakMap / WeakSet → NOT iterable, cannot remap. Keep as-is.
    if (value instanceof WeakMap || value instanceof WeakSet) {
        return undefined;
    }

    // 8. @serializable objects (incl. EventList, CallInfo) → shallow clone + recursively resolve $serializedTypes fields
    if (isSerializable(value) && value.$serializedTypes) {
        return resolveSerializableObject(value, objectMap);
    }

    // 9. Plain objects / Records → shallow clone, resolve each value
    if (isPlainObject(value)) {
        return resolvePlainObject(key, value, objectMap);
    }

    return undefined;
}

// ————————————————————————————————————————————————————————
// Internal Helpers
// ————————————————————————————————————————————————————————

function resolveComponentReference(value: IComponent, objectMap: InstantiateReferenceMap): object | undefined {
    const originalGameObject = value["gameObject"] as Object3D | undefined;
    if (!originalGameObject) return undefined;

    const id = originalGameObject.uuid;
    const newGameObject = objectMap[id]?.clone as Object3D | undefined;
    if (!newGameObject) {
        // Reference points to an object not in the cloned hierarchy (external)
        if (debug) console.log("Component reference did not change (external)", value);
        return undefined;
    }

    const index = originalGameObject.userData.components.indexOf(value);
    if (index >= 0 && newGameObject.isObject3D) {
        if (debug) console.log("Resolved component", id, "at index", index);
        return newGameObject.userData.components[index];
    }
    else {
        console.warn("Could not find component at expected index", value);
    }
    return undefined;
}

function resolveArray(key: string, arr: unknown[], objectMap: InstantiateReferenceMap): unknown[] {
    const result: unknown[] = [];
    for (let i = 0; i < arr.length; i++) {
        const entry = arr[i];
        if (entry === null || entry === undefined) {
            result.push(entry);
            continue;
        }
        // Skip primitives that can't be remapped (numbers, booleans)
        // but allow strings through for guid resolution
        if (typeof entry !== "object" && typeof entry !== "string") {
            result.push(entry);
            continue;
        }
        const resolved = resolveValue(key, entry, objectMap);
        result.push(resolved !== undefined ? resolved : entry);
    }
    return result;
}

function resolveMap(map: Map<unknown, unknown>, objectMap: InstantiateReferenceMap): Map<any, any> {
    const result = new Map();
    let didChange = false;
    for (const [mapKey, mapValue] of map) {
        let resolvedKey = mapKey;
        let resolvedValue = mapValue;

        if (typeof mapKey === "object" && mapKey !== null) {
            const rk = resolveValue("", mapKey, objectMap);
            if (rk !== undefined) { resolvedKey = rk; didChange = true; }
        }
        if (typeof mapValue === "object" && mapValue !== null) {
            const rv = resolveValue("", mapValue, objectMap);
            if (rv !== undefined) { resolvedValue = rv; didChange = true; }
        }
        result.set(resolvedKey, resolvedValue);
    }
    return didChange ? result : result; // always return new Map to prevent shared mutation
}

function resolveSet(set: Set<unknown>, objectMap: InstantiateReferenceMap): Set<any> {
    const result = new Set();
    for (const entry of set) {
        if (typeof entry === "object" && entry !== null) {
            const resolved = resolveValue("", entry, objectMap);
            result.add(resolved !== undefined ? resolved : entry);
        } else {
            result.add(entry);
        }
    }
    return result;
}

function resolveSerializableObject(value: unknown, objectMap: InstantiateReferenceMap): any | undefined {
    // Clone the serializable object to avoid mutating the original (which may be shared with source)
    const cloned = Object.assign(Object.create(Object.getPrototypeOf(value)), value);
    let didChange = false;
    for (const key in cloned.$serializedTypes) {
        const val = cloned[key];
        if (val === null || val === undefined) continue;
        if (typeof val === "object") {
            if (debug) console.log("Recursively resolve references for", key, val);
            const resolved = resolveValue(key, val, objectMap);
            if (resolved !== undefined) {
                cloned[key] = resolved;
                didChange = true;
            }
        }
    }
    return didChange ? cloned : undefined;
}

function resolvePlainObject(_parentKey: string, obj: Record<string, unknown>, objectMap: InstantiateReferenceMap): Record<string, unknown> | undefined {
    let didChange = false;
    const clone = { ...obj };
    for (const key of Object.keys(clone)) {
        const val = clone[key];
        if (val === null || val === undefined) continue;
        // Skip primitives that can't be remapped, but allow strings for guid resolution
        if (typeof val !== "object" && typeof val !== "string") continue;
        const resolved = resolveValue(key, val, objectMap);
        if (resolved !== undefined) {
            clone[key] = resolved;
            didChange = true;
        }
    }
    return didChange ? clone : undefined;
}

function isPlainObject(obj: unknown): obj is Record<string, unknown> {
    if (typeof obj !== "object" || obj === null) return false;
    const proto = Object.getPrototypeOf(obj);
    return proto === Object.prototype || proto === null;
}

/** Returns true if the object is a three.js value type that should be cloned (not remapped) */
function isCloneableValueType(value: object): value is { clone(): object } {
    return (value as Vector2).isVector2 === true ||
        (value as Vector3).isVector3 === true ||
        (value as Vector4).isVector4 === true ||
        (value as Quaternion).isQuaternion === true ||
        (value as Euler).isEuler === true ||
        (value as Color).isColor === true ||
        (value as Matrix2).isMatrix2 === true ||
        (value as Matrix3).isMatrix3 === true ||
        (value as Matrix4).isMatrix4 === true;
}

