import { BufferGeometry, Group, Mesh, Object3D, Vector3 } from "three"

import { isDevEnvironment } from "../engine/debug/index.js";
import { addComponent } from "../engine/engine_components.js";
import { Gizmos } from "../engine/engine_gizmos.js";
import type { PhysicsMaterial } from "../engine/engine_physics.types.js";
import { serializable } from "../engine/engine_serialization_decorator.js";
import { getBoundingBox } from "../engine/engine_three_utils.js";
import type { IBoxCollider, ICollider, ISphereCollider } from "../engine/engine_types.js";
import { validate } from "../engine/engine_util_decorator.js";
import { getParam, unwatchWrite, watchWrite } from "../engine/engine_utils.js";
import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive.js";
import { Behaviour } from "./Component.js";
import { Rigidbody } from "./RigidBody.js";

/**
 * Collider is the base class for all physics collision shapes.   
 * Colliders define the physical boundary of objects for collision detection.   
 * 
 * ![](https://cloud.needle.tools/-/media/slYWnXyaxdlrCqu8GP_lFQ.gif)
 *
 * **Usage with Rigidbody:**  
 * - Add a collider to define collision shape  
 * - Add a {@link Rigidbody} to the same or parent object for physics simulation  
 * - Without Rigidbody, collider acts as static geometry  
 *
 * **Trigger mode:**  
 * Set `isTrigger = true` for detection without physical collision.  
 * Triggers fire `onTriggerEnter`, `onTriggerStay`, `onTriggerExit` events.  
 *
 * **Collision filtering:**  
 * Use `membership` and `filter` arrays to control which objects collide.  
 *
 * @example Add a box collider to an object  
 * ```ts
 * const collider = myObject.addComponent(BoxCollider);
 * collider.size = new Vector3(1, 2, 1);
 * collider.center = new Vector3(0, 1, 0);
 * ```
 *
 * - Example: https://samples.needle.tools/physics-basic
 * - Example: https://samples.needle.tools/physics-playground
 *
 * @summary Physics collider base class
 * @category Physics
 * @group Components
 * @see {@link BoxCollider} for box-shaped colliders
 * @see {@link SphereCollider} for sphere-shaped colliders
 * @see {@link CapsuleCollider} for capsule-shaped colliders
 * @see {@link MeshCollider} for mesh-based colliders
 * @see {@link Rigidbody} for physics simulation
 * @link https://engine.needle.tools/samples/?room=needle272&overlay=samples&tag=physics
 * @link https://engine.needle.tools/samples-uploads/basic-physics/?showcolliders
 */
export abstract class Collider extends Behaviour implements ICollider {

    /**
     * Identifies this component as a collider.
     * @internal
     */
    get isCollider(): any {
        return true;
    }

    /**
     * The {@link Rigidbody} that this collider is attached to. This handles the physics simulation for this collider.
     */
    @serializable(Rigidbody)
    attachedRigidbody: Rigidbody | null = null;

    /**
     * When `true` the collider will not be used for collision detection but will still trigger events.
     * Trigger colliders can trigger events when other colliders enter their space, without creating a physical response/collision.
     */
    @serializable()
    isTrigger: boolean = false;

    /**
     * The physics material that defines physical properties of the collider such as friction and bounciness.
     */
    @serializable()
    sharedMaterial?: PhysicsMaterial;

    /**
     * The layers that this collider belongs to. Used for filtering collision detection.
     * @default [0]
     */
    @serializable()
    membership: number[] = [0];

    /**
     * The layers that this collider will interact with. Used for filtering collision detection.
     */
    @serializable()
    filter?: number[];

    /** @internal */
    awake() {
        super.awake();
        if (!this.attachedRigidbody)
            this.attachedRigidbody = this.gameObject.getComponentInParent(Rigidbody);
    }
    /** @internal */
    start() {
        if (!this.attachedRigidbody)
            this.attachedRigidbody = this.gameObject.getComponentInParent(Rigidbody);
    }
    /** @internal */
    onEnable() {
        // a rigidbody is not assigned if we export an asset
        if (!this.attachedRigidbody)
            this.attachedRigidbody = this.gameObject.getComponentInParent(Rigidbody);
    }
    /** @internal */
    onDisable() {
        this.context.physics.engine?.removeBody(this);
    }

    /**
     * Returns the underlying physics body from the physics engine.
     * Only available if the component is enabled and active in the scene.
     */
    get body() {
        return this.context.physics.engine?.getBody(this);
    }

    /**
     * Updates the collider's properties in the physics engine.
     * Use this when you've changed collider properties and need to sync with the physics engine.
     */
    updateProperties = () => {
        this.context.physics.engine?.updateProperties(this);
    }

    /**
     * Updates the physics material in the physics engine.
     * Call this after changing the sharedMaterial property.
     */
    updatePhysicsMaterial() {
        this.context.physics.engine?.updatePhysicsMaterial(this);
    }
}

/**
 * SphereCollider represents a sphere-shaped collision volume.  
 * Efficient and suitable for balls, projectiles, or approximate collision bounds.  
 * 
 * ![](https://cloud.needle.tools/-/media/slYWnXyaxdlrCqu8GP_lFQ.gif)
 *
 * @example Create a bouncing ball
 * ```ts
 * const sphere = ball.addComponent(SphereCollider);
 * sphere.radius = 0.5;
 * const rb = ball.addComponent(Rigidbody);
 * rb.mass = 1;
 * ```
 *
 * - Example: https://samples.needle.tools/physics-basic
 *
 * @summary Sphere-shaped physics collider
 * @category Physics
 * @group Components
 * @see {@link Collider} for base collider functionality
 * @see {@link CapsuleCollider} for elongated sphere shapes
 */
export class SphereCollider extends Collider implements ISphereCollider {

    /**
     * The radius of the sphere collider.
     */
    @validate()
    @serializable()
    radius: number = .5;

    /**
     * The center position of the sphere collider relative to the transform's position.
     */
    @serializable(Vector3)
    center: Vector3 = new Vector3(0, 0, 0);

    /**
     * Registers the sphere collider with the physics engine and sets up scale change monitoring.
     */
    onEnable() {
        super.onEnable();
        this.context.physics.engine?.addSphereCollider(this);
        watchWrite(this.gameObject.scale, this.updateProperties);
    }

    /**
     * Removes scale change monitoring when the collider is disabled.
     */
    onDisable(): void {
        super.onDisable();
        unwatchWrite(this.gameObject.scale, this.updateProperties);
    }

    /**
     * Updates collider properties when validated in the editor or inspector.
     */
    onValidate(): void {
        this.updateProperties();
    }
}

/**
 * BoxCollider represents a box-shaped (cuboid) collision volume.  
 * Most common collider type, efficient for walls, floors, crates, and rectangular objects.  
 * 
 * ![](https://cloud.needle.tools/-/media/slYWnXyaxdlrCqu8GP_lFQ.gif)
 *
 * @example Create a floor collider
 * ```ts
 * const box = floor.addComponent(BoxCollider);
 * box.size = new Vector3(10, 0.1, 10);
 * box.center = new Vector3(0, -0.05, 0);
 * ```
 *
 * @example Auto-fit to mesh geometry
 * ```ts
 * const collider = BoxCollider.add(myMesh, { rigidbody: true });
 * // Collider size is automatically set from mesh bounds
 * ```
 *
 * - Example: https://samples.needle.tools/physics-basic
 *
 * @summary Box-shaped physics collider
 * @category Physics
 * @group Components
 * @see {@link Collider} for base collider functionality
 * @see {@link SphereCollider} for sphere shapes
 */
export class BoxCollider extends Collider implements IBoxCollider {

    /**
     * Creates and adds a BoxCollider to the given object.
     * @param obj The object to add the collider to
     * @param opts Configuration options for the collider and optional rigidbody
     * @returns The newly created BoxCollider
     */
    static add(obj: Mesh | Object3D, opts?: { rigidbody: boolean, debug?: boolean }) {
        const collider = addComponent(obj, BoxCollider);
        collider.autoFit();

        if (opts?.rigidbody === true) {
            addComponent(obj, Rigidbody, { isKinematic: false });
        }
        return collider;
    }

    /**
     * The size of the box collider along each axis.
     */
    @validate()
    @serializable(Vector3)
    size: Vector3 = new Vector3(1, 1, 1);

    /**
     * The center position of the box collider relative to the transform's position.
     */
    @serializable(Vector3)
    center: Vector3 = new Vector3(0, 0, 0);

    /**
     * Registers the box collider with the physics engine and sets up scale change monitoring.
     * @internal
     */
    onEnable() {
        super.onEnable();
        this.context.physics.engine?.addBoxCollider(this, this.size);
        watchWrite(this.gameObject.scale, this.updateProperties);
    }

    /**
     * Removes scale change monitoring when the collider is disabled.
     * @internal
     */
    onDisable(): void {
        super.onDisable();
        unwatchWrite(this.gameObject.scale, this.updateProperties);
    }

    /**
     * Updates collider properties when validated in the editor or inspector.
     * @internal
     */
    onValidate(): void {
        this.updateProperties();
    }

    /**
     * Automatically fits the collider to the geometry of the object.
     * Sets the size and center based on the object's bounding box.
     * @param opts Options object with a debug flag to visualize the bounding box
     */
    autoFit(opts?: { debug?: boolean }) {
        const obj = this.gameObject;

        // we need to transform the object into identity
        // because the physics collider will correctly apple the object's transform again
        // if we don't do it here we will have the transform applied twice
        const originalPosition = obj.position.clone();
        const originalQuaternion = obj.quaternion.clone();
        const originalScale = obj.scale.clone();
        const originalParent = obj.parent;
        obj.position.set(0, 0, 0);
        obj.quaternion.set(0, 0, 0, 1);
        obj.scale.set(1, 1, 1);
        obj.parent = null;
        obj.updateMatrix();
        const bb = getBoundingBox([obj]);
        obj.position.copy(originalPosition);
        obj.quaternion.copy(originalQuaternion);
        obj.scale.copy(originalScale);
        obj.parent = originalParent;

        if (opts?.debug === true) Gizmos.DrawWireBox3(bb, 0xffdd00, 20);

        // if (!obj.geometry.boundingBox) obj.geometry.computeBoundingBox();
        // const bb = obj.geometry.boundingBox!;
        this.size = bb!.getSize(new Vector3()) || new Vector3(1, 1, 1);
        this.center = bb!.getCenter(new Vector3()) || new Vector3(0, 0, 0);
        if (this.size.length() <= 0) {
            this.size.set(0.01, 0.01, 0.01);
        }
    }
}

/**
 * MeshCollider creates a collision shape from a mesh geometry.  
 * Allows for complex collision shapes that match the exact geometry of an object.  
 * 
 * ![](https://cloud.needle.tools/-/media/slYWnXyaxdlrCqu8GP_lFQ.gif)
 * 
 * - Example: https://samples.needle.tools/physics-basic
 * - Example: https://samples.needle.tools/physics-playground
 * - Example: https://samples.needle.tools/physics-&-animation 
 * 
 * @category Physics
 * @group Components
 */
export class MeshCollider extends Collider {

    /**
     * The mesh that is used to create the collision shape.
     * If not set, the collider will try to use the mesh of the object it's attached to.
     */
    @serializable(Mesh)
    sharedMesh?: Mesh;

    /**
     * When `true` the collider is treated as a solid object without holes.
     * Set to `false` if you want this mesh collider to be able to contain other objects.
     */
    @serializable()
    convex: boolean = false;

    /**
     * Creates and registers the mesh collider with the physics engine.
     * Handles both individual meshes and mesh groups.
     */
    onEnable() {
        super.onEnable();
        if (!this.context.physics.engine) return;

        if (!this.sharedMesh?.isMesh) {
            // HACK using the renderer mesh
            if (this.gameObject instanceof Mesh || this.gameObject instanceof Group) {
                // We're passing a group in here as well, the code below handles that correctly
                this.sharedMesh = this.gameObject as Mesh;
            }
        }

        const LOD = 0;

        if (this.sharedMesh?.isMesh) {
            this.context.physics.engine.addMeshCollider(this, this.sharedMesh, this.convex);
            NEEDLE_progressive.assignMeshLOD(this.sharedMesh, LOD).then(res => {
                if (res && this.activeAndEnabled && this.context.physics.engine && this.sharedMesh) {
                    this.context.physics.engine.removeBody(this);
                    this.sharedMesh.geometry = res;
                    this.context.physics.engine.addMeshCollider(this, this.sharedMesh, this.convex);
                }
            })
        }
        else {
            const group = this.sharedMesh as any as Group;
            if (group?.isGroup) {
                console.warn(`MeshCollider mesh is a group \"${this.sharedMesh?.name || this.gameObject.name}\", adding all children as colliders. This is currently not fully supported (colliders can not be removed from world again)`, this);
                const promises = new Array<Promise<BufferGeometry | null>>();
                for (const ch in group.children) {
                    const child = group.children[ch] as Mesh;
                    if (child.isMesh) {
                        this.context.physics.engine.addMeshCollider(this, child, this.convex);
                        promises.push(NEEDLE_progressive.assignMeshLOD(child, LOD));
                    }
                }
                Promise.all(promises).then(res => {
                    if (res.some(r => r) == false) return;
                    this.context.physics.engine?.removeBody(this);
                    const mesh = new Mesh();
                    for (const r of res) {
                        if (r && this.activeAndEnabled) {
                            mesh.geometry = r;
                            this.context.physics.engine?.addMeshCollider(this, mesh, this.convex);
                        }
                    }
                });
            }
            else {
                if (isDevEnvironment() || getParam("showcolliders")) {
                    console.warn(`[MeshCollider] A MeshCollider mesh is assigned to an unknown object on \"${this.gameObject.name}\", but it's neither a Mesh nor a Group. Please double check that you attached the collider component to the right object and report a bug otherwise!`, this);
                }
            }
        }
    }
}

/**
 * CapsuleCollider represents a capsule-shaped collision volume (cylinder with hemispherical ends).  
 * Ideal for character controllers and objects that need a rounded collision shape.  
 * 
 * ![](https://cloud.needle.tools/-/media/slYWnXyaxdlrCqu8GP_lFQ.gif)
 * 
 * - Example: https://samples.needle.tools/physics-basic
 * - Example: https://samples.needle.tools/physics-playground
 * - Example: https://samples.needle.tools/physics-&-animation 
 * 
 * @category Physics
 * @group Components
 */
export class CapsuleCollider extends Collider {
    /**
     * The center position of the capsule collider relative to the transform's position.
     */
    @serializable(Vector3)
    center: Vector3 = new Vector3(0, 0, 0);

    /**
     * The radius of the capsule's cylindrical body and hemispherical ends.
     */
    @serializable()
    radius: number = .5;

    /**
     * The total height of the capsule including both hemispherical ends.
     */
    @serializable()
    height: number = 2;

    /**
     * Registers the capsule collider with the physics engine.
     */
    onEnable() {
        super.onEnable();
        this.context.physics.engine?.addCapsuleCollider(this, this.height, this.radius);
    }
}