import { ComponentLifecycleEvents } from "./engine_components_internal.js";
import { activeInHierarchyFieldName } from "./engine_constants.js";
import { removeScriptFromContext, updateActiveInHierarchyWithoutEventCall } from "./engine_mainloop_utils.js";
import { InstantiateIdProvider } from "./engine_networking_instantiate.js";
import { Context, registerComponent } from "./engine_setup.js";
import { getParam } from "./engine_utils.js";
import { apply } from "./js-extensions/index.js";
const debug = getParam("debuggetcomponent");
const debugEnabled = () => {
    return debug || globalThis["NEEDLE_DEBUG_GETCOMPONENT"] === true;
};
function tryGetObject(obj) {
    if (obj === null || obj === undefined)
        return obj;
    if (obj.isObject3D)
        return obj;
    // handle threejs intersection object
    if (obj.object && obj.object.isObject3D)
        return obj.object;
    return obj;
}
export function removeComponent(go, componentInstance) {
    if (!go)
        return componentInstance;
    if (!go.userData.components)
        return componentInstance;
    const index = go.userData.components.indexOf(componentInstance);
    if (index < 0)
        return componentInstance;
    ComponentLifecycleEvents.dispatchComponentLifecycleEvent("removing-component", componentInstance);
    //@ts-ignore
    componentInstance.gameObject = null;
    go.userData.components.splice(index, 1);
    return componentInstance;
}
export function getOrAddComponent(go, typeName, init) {
    const comp = getComponent(go, typeName);
    if (comp)
        return comp;
    return addComponent(go, typeName, init);
}
const idProvider = new InstantiateIdProvider("addComponentIdProvider");
export function addNewComponent(obj, componentInstance, callAwake = true) {
    if (!obj) {
        new Error("Can not add componet to null object");
    }
    if (!obj.userData)
        obj.userData = {};
    if (!obj.userData.components)
        obj.userData.components = [];
    obj.userData.components.push(componentInstance);
    componentInstance.gameObject = obj;
    // TODO: currently add component does not ensure a new component instance has a guid
    if (componentInstance.guid === undefined || componentInstance.guid === "invalid") {
        componentInstance.guid = idProvider.generateUUID();
    }
    apply(obj);
    // register the component - make sure to provide the component instance context (if assigned)
    registerComponent(componentInstance, componentInstance.context);
    try {
        if (callAwake && componentInstance.__internalAwake) {
            updateActiveInHierarchyWithoutEventCall(obj);
            if (componentInstance.activeAndEnabled)
                componentInstance.__internalAwake();
        }
        ComponentLifecycleEvents.dispatchComponentLifecycleEvent("component-added", componentInstance);
    }
    catch (err) {
        console.error(err);
    }
    return componentInstance;
}
export function addComponent(obj, componentInstance, init, opts) {
    if (typeof componentInstance === "function") {
        const instance = new componentInstance();
        if (init)
            instance.__internalNewInstanceCreated(init);
        let callAwake = true;
        if (opts?.callAwake != undefined)
            callAwake = opts.callAwake;
        return addNewComponent(obj, instance, callAwake);
    }
    if (componentInstance.destroyed) {
        console.warn("Can not move/add a destroyed component", componentInstance);
        return componentInstance;
    }
    if (componentInstance.gameObject === obj)
        return componentInstance;
    // TODO: update raycast array
    if (componentInstance.gameObject && componentInstance.gameObject.userData?.components) {
        const index = componentInstance.gameObject.userData.components.indexOf(componentInstance);
        componentInstance.gameObject.userData.components.splice(index, 1);
    }
    if (!obj.userData)
        obj.userData = {};
    if (!obj.userData.components)
        obj.userData.components = [];
    else if (obj.userData.components.includes(componentInstance))
        return componentInstance;
    // TODO: do we want to disable and enable when moving?
    // componentInstance.__internalDisable();
    obj.userData.components.push(componentInstance);
    componentInstance.gameObject = obj;
    // componentInstance.__internalEnable();
    // componentInstance.transform = obj;
    if (componentInstance.guid === undefined || componentInstance.guid === "invalid") {
        componentInstance.guid = idProvider.generateUUID();
    }
    if (init)
        componentInstance._internalInit(init);
    // Register the component - make sure to provide the component instance context (if assigned)
    registerComponent(componentInstance, componentInstance.context);
    return componentInstance;
}
export function destroyComponentInstance(componentInstance) {
    // console.log("destroy", componentInstance, componentInstance.onDestroy);
    // TODO: update raycast array
    if (componentInstance.gameObject && componentInstance.gameObject.userData.components) {
        const index = componentInstance.gameObject.userData.components.indexOf(componentInstance);
        componentInstance.gameObject.userData.components.splice(index, 1);
    }
    // should we call these methods frame delayed?
    if (componentInstance.__internalDisable)
        componentInstance.__internalDisable();
    removeScriptFromContext(componentInstance, componentInstance.context ?? Context.Current);
    componentInstance.destroy();
    // console.log("destroyed", componentInstance, "- go:", componentInstance.gameObject);
    //@ts-ignore
    componentInstance.gameObject = null;
}
let didWarnAboutComponentAccess = false;
function onGetComponent(obj, componentType, arr) {
    if (obj === null || obj === undefined)
        return null;
    if (!obj.isObject3D) {
        console.error("Object is not object3D");
        return null;
    }
    if (!(obj?.userData?.components))
        return null;
    if (typeof componentType === "string") {
        if (!didWarnAboutComponentAccess) {
            didWarnAboutComponentAccess = true;
            console.warn(`Accessing components by name is not supported.\nPlease use the component type instead. This may keep working in local development but it will fail when bundling your application.\n\nYou can import other modules your main module to get access to types\nor if you use npmdefs you can make types available globally using globalThis:\nhttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis`, componentType);
        }
    }
    if (debugEnabled())
        console.log("[onGetComponent] FIND", componentType);
    if (componentType === undefined || componentType === null)
        return null;
    // find component
    for (let i = 0; i < obj.userData.components.length; i++) {
        const component = obj.userData.components[i];
        let prototype = Object.getPrototypeOf(component);
        while (prototype) {
            if (prototype === componentType.prototype) {
                if (debugEnabled())
                    console.log("[onGetComponent] MATCH BY PROTOYPE", prototype);
                if (arr)
                    arr.push(component);
                else
                    return component;
            }
            prototype = Object.getPrototypeOf(prototype);
        }
    }
    if (!arr)
        return null;
    return arr;
}
/**
 * Searches for a given component type in the given object.
 * @param obj The object to search in.
 * @param componentType The type of the component to search for.
 * @returns The first component of the given type found in the given object.
 * @example
 * ```typescript
 * const myComponent = getComponent(myObject, MyComponent);
 * ```
 */
export function getComponent(obj, componentType) {
    const result = onGetComponent(obj, componentType);
    if (!result)
        return null;
    if (Array.isArray(result))
        return result[0];
    return result;
}
/**
 * Searches for a given component type in the children of the given object.
 * @param obj The object to start the search from - this object is also included in the search.
 * @param componentType The type of the component to search for.
 * @param arr An optional array to store the found components in. If not provided, a new array is created.
 * @param clearArray If true, the array is cleared before storing the found components. Default is true.
 * @returns An array of components of the given type found in the children of the given object.
 * @example
 * ```typescript
 * const myComponents = getComponents(myObject, MyComponent);
 * ```
 */
export function getComponents(obj, componentType, arr, clearArray = true) {
    if (!arr)
        arr = [];
    if (clearArray)
        arr.length = 0;
    onGetComponent(obj, componentType, arr);
    return arr;
}
/**
 * Searches for a given component type in the children of the given object.
 * @param obj The object to start the search from - this object is also included in the search.
 * @param componentType The type of the component to search for.
 * @param includeInactive If true, also inactive components are returned. Default is true.
 * @returns The first component of the given type found in the children of the given object.
 * @example
 * ```typescript
 * const myComponent = getComponentInChildren(myObject, MyComponent);
 * ```
 */
export function getComponentInChildren(obj, componentType, includeInactive = false) {
    if (includeInactive === false && obj[activeInHierarchyFieldName] === false)
        return null;
    const res = getComponent(obj, componentType);
    if (includeInactive === false && (res?.enabled === false || res?.activeAndEnabled === false))
        return null;
    if (res)
        return res;
    for (let i = 0; i < obj?.children?.length; i++) {
        const res = getComponentInChildren(obj.children[i], componentType, includeInactive);
        if (res)
            return res;
    }
    return null;
}
/**
 * Searches for a given component type in the children of the given object.
 * @param obj The object to start the search from - this object is also included in the search.
 * @param componentType The type of the component to search for.
 * @param arr An optional array to store the found components in. If not provided, a new array is created.
 * @param clearArray If true, the array is cleared before storing the found components. Default is true.
 * @returns An array of components of the given type found in the children of the given object.
 * @example
 * ```typescript
 * const myComponents = getComponentsInChildren(myObject, MyComponent);
 * ```
 */
export function getComponentsInChildren(obj, componentType, arr, clearArray = true) {
    if (!arr)
        arr = [];
    if (clearArray)
        arr.length = 0;
    getComponents(obj, componentType, arr, false);
    for (let i = 0; i < obj?.children?.length; i++) {
        getComponentsInChildren(obj.children[i], componentType, arr, false);
    }
    return arr;
}
/**
 * Searches for a given component type in the parent hierarchy of the given object.
 * @param obj The object to start the search from - this object is also included in the search.
 * @param componentType The type of the component to search for.
 * @returns The first component of the given type found in the parent hierarchy of the given object.
 * @example
 * ```typescript
 * const myComponent = getComponentInParent(myObject, MyComponent);
 * ```
 */
export function getComponentInParent(obj, componentType) {
    if (!obj)
        return null;
    if (Array.isArray(obj)) {
        for (let i = 0; i < obj.length; i++) {
            const o = tryGetObject(obj[i]);
            const res = getComponentInParent(o, componentType);
            if (res)
                return res;
        }
        return null;
    }
    // console.log(obj);
    const res = getComponent(obj, componentType);
    if (res)
        return res;
    if (obj.parent)
        return getComponentInParent(obj.parent, componentType);
    return null;
}
/**
 * Searches for a given component type in the parent hierarchy of the given object.
 * @param obj The object to start the search from - this object is also included in the search.
 * @param componentType The type of the component to search for.
 * @param arr An optional array to store the found components in. If not provided, a new array is created.
 * @param clearArray If true, the array is cleared before storing the found components. Default is true.
 * @returns An array of components of the given type found in the parent hierarchy of the given object.
 * @example
 * ```typescript
 * const myComponents = getComponentsInParent(myObject, MyComponent);
 * ```
 */
export function getComponentsInParent(obj, componentType, arr, clearArray = true) {
    if (!arr)
        arr = [];
    if (clearArray)
        arr.length = 0;
    if (!obj)
        return arr;
    getComponents(obj, componentType, arr, false);
    if (obj.parent)
        return getComponentsInParent(obj.parent, componentType, arr, false);
    return arr;
}
/**
 * Searches the the scene for a component of the given type.
 * If the contextOrScene is not provided, the current context is used.
 * @param type The type of the component to search for.
 * @param contextOrScene The context or scene to search in. If not provided, the current context is used.
 * @param includeInactive If true, also inactive components are returned. Default is true.
 * @returns The first component of the given type found in the scene or null if none was found.
 * @example
 * ```typescript
 * const myComponent = findObjectOfType(MyComponent);
 * ```
 */
export function findObjectOfType(type, contextOrScene = undefined, includeInactive = true) {
    if (!type)
        return null;
    if (!contextOrScene) {
        contextOrScene = Context.Current;
        if (!contextOrScene) {
            console.error("Can not search object without any needle context or scene!!!");
            return null;
        }
    }
    let scene = contextOrScene;
    if (!scene.isScene)
        scene = contextOrScene?.scene;
    if (!scene)
        return null;
    const res = getComponentInChildren(scene, type, includeInactive);
    if (res)
        return res;
    return null;
}
/**
 * Searches the the scene for all components of the given type.
 * If the contextOrScene is not provided, the current context is used.
 * @param type The type of the component to search for.
 * @param contextOrScene The context or scene to search in. If not provided, the current context is used.
 * @example
 * ```typescript
 * const myComponents = findObjectsOfType(MyComponent);
 * ```
 */
export function findObjectsOfType(type, array, contextOrScene = undefined) {
    if (!type)
        return array ?? [];
    if (!array)
        array = [];
    array.length = 0;
    if (!contextOrScene) {
        contextOrScene = Context.Current;
        if (!contextOrScene) {
            console.error("Can not search object without any needle context or scene!!!");
            return array;
        }
    }
    if ("scene" in contextOrScene)
        contextOrScene = contextOrScene.scene;
    const scene = contextOrScene;
    if (!scene)
        return array;
    getComponentsInChildren(scene, type, array, false);
    return array;
}
//# sourceMappingURL=engine_components.js.map