import { Matrix4, Object3D, Quaternion, Vector3 } from "three";
import { TransformControlsGizmo } from "three/examples/jsm/controls/TransformControls.js";

import { addComponent, getComponent, getComponentInChildren, getComponentInParent, getComponents, getComponentsInChildren, getComponentsInParent, getOrAddComponent, removeComponent } from "../../engine/engine_components.js";
import { destroy, isActiveSelf, setActive } from "../../engine/engine_gameobject.js";
import {
    getTempQuaternion,
    getTempVector,
    getWorldPosition,
    getWorldQuaternion,
    getWorldRotation,
    getWorldScale,
    setWorldPosition,
    setWorldQuaternion,
    setWorldRotation,
    setWorldScale
}
    from "../../engine/engine_three_utils.js";
import type { ComponentInit, Constructor, ConstructorConcrete, HideFlags, IComponent as Component, IComponent } from "../../engine/engine_types.js";
import { NEEDLE_ENGINE_FEATURE_FLAGS } from "../engine_feature_flags.js";
import { markHierarchyDirty } from "../engine_mainloop_utils.js";
import { applyPrototypeExtensions, registerPrototypeExtensions } from "./ExtensionUtils.js";


// #region Type Declarations

// NOTE: keep in sync with method declarations below
declare module 'three' {
    export interface Object3D {

        get guid(): string | undefined;
        set guid(value: string | undefined);

        /**
         * Allows to control e.g. if an object should be exported
         */
        hideFlags: HideFlags;

        /**
         * If false the object will be ignored for raycasting (e.g. pointer events). Default is true.
         * @default true
         */
        raycastAllowed: boolean;

        /**
         * Set a raycast preference for the object: 
         * - `lod` will use the raycast mesh lod if available (default). This is usually a simplified mesh for raycasting.  
         * - `bounds` will use the bounding box of the object for raycasting. This is very fast but not very accurate.  
         * - `full` will use the full mesh for raycasting. This is the most accurate but also the slowest option.
         * 
         * **NOTE:** Needle Engine's Raycast system will use Mesh BVH by default - so event 'full' is usually faster than default three.js raycasting.
         */
        raycastPreference?: 'lod' | 'bounds' | 'full';


        /**
         * Add a Needle Engine component to the {@link Object3D}.
         * @param comp The component instance or constructor to add.
         * @param init Optional initialization data for the component.
         * @returns The added component instance.
         * @example Directly pass in constructor and properties:
         * ```ts
         * const obj = new Object3D();
         * obj.addComponent(MyComponent, { myProperty: 42 });
         * ```
         * @example Create a component instance, assign properties and then add it:
         * ```ts
         * const obj = new Object3D();
         * const comp = new MyComponent();
         * comp.myProperty = 42;
         * obj.addComponent(comp);
         * ```
         */
        addComponent<T extends IComponent>(comp: T | ConstructorConcrete<T>, init?: ComponentInit<T>): T;
        /**
         * Remove a Needle Engine component from the {@link Object3D}.
         */
        removeComponent(inst: IComponent): IComponent;
        /**
         * Get or add a Needle Engine component to the Object3D.
         * If the component already exists, it will be returned. Otherwise, a new component will be added.
         * @param typeName The component constructor to get or add.
         * @param init Optional initialization data for the component.
         * @returns The component instance.
         */
        getOrAddComponent<T extends IComponent>(typeName: ConstructorConcrete<T>, init?: ComponentInit<T>): T;
        /**
         * Get a Needle Engine component from the {@link Object3D}.
         * @returns The component instance or null if not found.
         */
        getComponent<T extends IComponent>(type: Constructor<T>): T | null;
        /**
         * Get all components of a specific type from the {@link Object3D}.
         * @param arr Optional array to fill with the found components.
         * @returns An array of components.
         */
        getComponents<T extends IComponent>(type: Constructor<T>, arr?: []): T[];
        /**
         * Get a Needle Engine component from the {@link Object3D} or its children. This will search on the current Object and all its children.
         * @param type The type of the component to search for.
         * @param includeInactive If true, also inactive components are considered. Default is false.
         * @returns The component instance or null if not found.
         */
        getComponentInChildren<T extends IComponent>(type: Constructor<T>, includeInactive?: boolean): T | null;
        /**
         * Get all components of a specific type from the {@link Object3D} or its children. This will search on the current Object and all its children.
         * @param arr Optional array to fill with the found components.
         * @returns An array of components.
         */
        getComponentsInChildren<T extends IComponent>(type: Constructor<T>, arr?: []): T[];
        /**
         * Get a Needle Engine component from the {@link Object3D} or its parents. This will search on the current Object and all its parents.
         * @param type The type of the component to search for.
         * @param includeInactive If true, also inactive components are considered. Default is false.
         * @returns The component instance or null if not found.
         */
        getComponentInParent<T extends IComponent>(type: Constructor<T>, includeInactive?: boolean): T | null;
        /**
         * Get all Needle Engine components of a specific type from the {@link Object3D} or its parents. This will search on the current Object and all its parents.
         * @param arr Optional array to fill with the found components.
         * @returns An array of components.
         */
        getComponentsInParent<T extends IComponent>(type: Constructor<T>, arr?: []): T[];

        /**
         * Destroys the {@link Object3D} and all its Needle Engine components.
         */
        destroy(): void;

        /**
         * Get or set the world position of the {@link Object3D}.   
         * Added by Needle Engine.
         */
        worldPosition: Vector3;
        /**
         * Get or set the world quaternion of the {@link Object3D}.  
         * Added by Needle Engine.
         */
        worldQuaternion: Quaternion;
        /**
         * Get or set the world rotation of the {@link Object3D}.  
         * Added by Needle Engine.
         */
        worldRotation: Vector3;
        /**
         * Get or set the world scale of the {@link Object3D}.  
         * Added by Needle Engine.
         */
        worldScale: Vector3;
        /**
         * Get the world forward vector of the {@link Object3D}.  
         * Added by Needle Engine.
         */
        get worldForward(): Vector3;
        set worldForward(v: Vector3);
        /**
         * Get the world right vector of the {@link Object3D}.  
         * Added by Needle Engine.
         */
        get worldRight(): Vector3;
        /**
         * Get the world up vector of the {@link Object3D}.  
         * Added by Needle Engine.
         */
        get worldUp(): Vector3;


        /**
         * Check if the given object is contained in the hierarchy of this object or if it's the same object.
         * @param object The object to check.
         * @returns True if the object is contained in the hierarchy, false otherwise.
         */
        contains(object: Object3D | null | undefined): boolean;
    }
}


/** 
 * @internal
 * used to decorate cloned object3D objects with the same added components defined above 
 **/
export function apply(object: Object3D) {
    if (object && object.isObject3D === true) {
        applyPrototypeExtensions(object, Object3D);
    }
}

let initialized = false;

export function initObject3DExtensions() {
    if (initialized) return;
    initialized = true;

    if (NEEDLE_ENGINE_FEATURE_FLAGS.experimentalSmartHierarchyUpdate) {

        const addFn = Object3D.prototype.add;
        Object3D.prototype.add = function (...args: any) {
            markHierarchyDirty();
            return addFn.apply(this, args);
        }

        const attachFn = Object3D.prototype.attach;
        Object3D.prototype.attach = function (...args: any) {
            markHierarchyDirty();
            return attachFn.apply(this, args);
        }

        const removeFn = Object3D.prototype.remove;
        Object3D.prototype.remove = function (...args: any) {
            markHierarchyDirty();
            return removeFn.apply(this, args);
        }
    }

    // #region Prototype Method Implementations

    Object3D.prototype["SetActive"] = function (active: boolean) {
        this.visible = active;
    }
    // e.g. when called via a UnityEvent
    Object3D.prototype["setActive"] = function (active: boolean) {
        this.visible = active;
    }

    Object3D.prototype["destroy"] = function () {
        destroy(this);
    }

    Object3D.prototype["addComponent"] = function <T extends IComponent>(comp: T | ConstructorConcrete<T>, init?: ComponentInit<T>) {
        return addComponent(this, comp, init);
    }

    Object3D.prototype["addNewComponent"] = function <T extends Component>(type: ConstructorConcrete<T>, init?: ComponentInit<T>) {
        return addComponent(this, type, init);
    }

    Object3D.prototype["removeComponent"] = function (inst: Component) {
        return removeComponent(this, inst);
    }

    Object3D.prototype["getOrAddComponent"] = function <T extends IComponent>(typeName: ConstructorConcrete<T>, init?: ComponentInit<T>): T {
        return getOrAddComponent<T>(this, typeName, init);
    }

    Object3D.prototype["getComponent"] = function <T extends IComponent>(type: Constructor<T>) {
        return getComponent(this, type);
    }

    Object3D.prototype["getComponents"] = function <T extends IComponent>(type: Constructor<T>, arr?: []) {
        return getComponents(this, type, arr);
    }

    Object3D.prototype["getComponentInChildren"] = function <T extends IComponent>(type: Constructor<T>, includeInactive: boolean = false) {
        return getComponentInChildren(this, type, includeInactive);
    }

    Object3D.prototype["getComponentsInChildren"] = function <T extends IComponent>(type: Constructor<T>, arr?: []) {
        return getComponentsInChildren(this, type, arr);
    }

    Object3D.prototype["getComponentInParent"] = function <T extends IComponent>(type: Constructor<T>, includeInactive: boolean = false) {
        return getComponentInParent(this, type, includeInactive);
    }

    Object3D.prototype["getComponentsInParent"] = function <T extends IComponent>(type: Constructor<T>, arr?: []) {
        return getComponentsInParent(this, type, arr);
    }

    // #region Prototype Property Implementations

    // this is a fix to allow gameObject active animation be applied to a three object
    if (!Object.getOwnPropertyDescriptor(Object3D.prototype, "activeSelf")) {
        Object.defineProperty(Object3D.prototype, "activeSelf", {
            get: function () {
                return isActiveSelf(this)
            },
            set: function (val: boolean | number) {
                setActive(this, val);
            }
        });
    }


    if (!Object.getOwnPropertyDescriptor(Object3D.prototype, "raycastAllowed")) {
        Object.defineProperty(Object3D.prototype, "raycastAllowed", {
            get: function () {
                return this.userData && this.userData.raycastAllowed !== false;
            },
            set: function (val: boolean) {
                const self = this as Object3D;
                if (!self.userData) self.userData = {};
                self.userData.raycastAllowed = val;
            }
        });
    }


    if (!Object.getOwnPropertyDescriptor(Object3D.prototype, "worldPosition")) {
        Object.defineProperty(Object3D.prototype, "worldPosition", {
            get: function () {
                // TODO: would be great to remove this - just a workaround because the TransformControlsGizmo also defines this
                if (this instanceof TransformControlsGizmo) {
                    return getWorldPosition(this["object"]);
                }
                return getWorldPosition(this);
            },
            set: function (val: Vector3) {
                setWorldPosition(this, val)
            }
        });
    }

    if (!Object.getOwnPropertyDescriptor(Object3D.prototype, "worldQuaternion")) {
        Object.defineProperty(Object3D.prototype, "worldQuaternion", {
            get: function () {
                if (this instanceof TransformControlsGizmo) {
                    return getWorldQuaternion(this["object"]);
                }
                return getWorldQuaternion(this);
            },
            set: function (val: Quaternion) {
                setWorldQuaternion(this, val)
            }
        });
    }

    if (!Object.getOwnPropertyDescriptor(Object3D.prototype, "worldRotation")) {
        Object.defineProperty(Object3D.prototype, "worldRotation", {
            get: function () {
                return getWorldRotation(this);
            },
            set: function (val: Vector3) {
                setWorldRotation(this, val)
            }
        });
    }

    if (!Object.getOwnPropertyDescriptor(Object3D.prototype, "worldScale")) {
        Object.defineProperty(Object3D.prototype, "worldScale", {
            get: function () {
                return getWorldScale(this);
            },
            set: function (val: Vector3) {
                setWorldScale(this, val)
            }
        });
    }

    const tempMatrix = new Matrix4();
    const zero = new Vector3(0, 0, 0);
    const up = new Vector3(0, 1, 0);
    if (!Object.getOwnPropertyDescriptor(Object3D.prototype, "worldForward")) {
        Object.defineProperty(Object3D.prototype, "worldForward", {
            get: function () {
                return getTempVector().set(0, 0, 1).applyQuaternion(getWorldQuaternion(this));
            },
            set: function (v: Vector3) {
                const quat = getTempQuaternion().setFromRotationMatrix(tempMatrix.lookAt(zero.set(0, 0, 0), v, up.set(0, 1, 0)));
                this.worldQuaternion = quat;
            }
        });
    }
    if (!Object.getOwnPropertyDescriptor(Object3D.prototype, "worldRight")) {
        Object.defineProperty(Object3D.prototype, "worldRight", {
            get: function () {
                return getTempVector().set(1, 0, 0).applyQuaternion(getWorldQuaternion(this));
            },
        });
    }
    if (!Object.getOwnPropertyDescriptor(Object3D.prototype, "worldUp")) {
        Object.defineProperty(Object3D.prototype, "worldUp", {
            get: function () {
                return getTempVector().set(0, 1, 0).applyQuaternion(getWorldQuaternion(this));
            },
        });
    }



    if (!Object.getOwnPropertyDescriptor(Object3D.prototype, "contains")) {
        Object.defineProperty(Object3D.prototype, "contains", {
            value: function (object: Object3D | null | undefined): boolean {
                if (!object) return false;
                if (this === object) return true;
                for (const child of this.children) {
                    if (child.contains(object)) return true;
                }
                return false;
            }
        });
    }

    // do this after adding the component extensions
    registerPrototypeExtensions(Object3D);
}
