import { BufferGeometry, Group, Mesh, Object3D, Vector3 } from "three"

import { addComponent } from "../engine/engine_components.js";
import type { PhysicsMaterial } from "../engine/engine_physics.types.js";
import { serializable } from "../engine/engine_serialization_decorator.js";
import { getWorldScale } from "../engine/engine_three_utils.js";
// import { IColliderProvider, registerColliderProvider } from "../engine/engine_physics.js";
import type { IBoxCollider, ICollider, ISphereCollider } from "../engine/engine_types.js";
import { validate } from "../engine/engine_util_decorator.js";
import { 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 colliders. A collider is a physical shape that is used to detect collisions with other objects in the scene.  
 * Colliders are used in combination with a Rigidbody to create physical interactions between objects.  
 * Colliders are registered with the physics engine when they are enabled and removed when they are disabled.
 * @category Physics  
 * @group Components
 */
export class Collider extends Behaviour implements ICollider {

    /** @internal */
    get isCollider(): any {
        return true;
    }

    /**
     * The Rigidbody that this collider is attached to.
     */
    @serializable(Rigidbody)
    attachedRigidbody: Rigidbody | null = null;

    /**
     * When `true` the collider will not be used for collision detection but will still trigger events.
     */
    @serializable()
    isTrigger: boolean = false;

    /**
     * The physics material that is used for the collider. This material defines physical properties of the collider such as friction and bounciness.
     */
    @serializable()
    sharedMaterial?: PhysicsMaterial;

    /**
     * The layers that the collider is assigned to.
     */
    @serializable()
    membership: number[] = [0];

    /**
     * The layers that the collider will interact with.
     * @inheritdoc
     */
    @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 (if any) - the component must be enabled and active in the scene */
    get body() {
        return this.context.physics.engine?.getBody(this);
    }

    /**
     * Apply the collider properties to the physics engine.
     */
    updateProperties = () => {
        this.context.physics.engine?.updateProperties(this);
    }

    /** Requests an update of the physics material in the physics engine */
    updatePhysicsMaterial(){
        this.context.physics.engine?.updatePhysicsMaterial(this);

    }
}

/**
 * SphereCollider is a collider that represents a sphere shape.
 * @category Physics
 * @group Components
 */
export class SphereCollider extends Collider implements ISphereCollider {

    @validate()
    @serializable()
    radius: number = .5;

    @serializable(Vector3)
    center: Vector3 = new Vector3(0, 0, 0);

    onEnable() {
        super.onEnable();
        this.context.physics.engine?.addSphereCollider(this);
        watchWrite(this.gameObject.scale, this.updateProperties);
    }

    onDisable(): void {
        super.onDisable();
        unwatchWrite(this.gameObject.scale, this.updateProperties);
    }

    onValidate(): void {
        this.updateProperties();
    }
}

/**
 * BoxCollider is a collider that represents a box shape.
 * @category Physics
 * @group Components
 */
export class BoxCollider extends Collider implements IBoxCollider {

    static add(obj: Mesh, opts?: { rigidbody: boolean }) {
        const collider = new BoxCollider();

        if (!obj.geometry.boundingBox) obj.geometry.computeBoundingBox();
        const bb = obj.geometry.boundingBox!;
        collider.size = bb!.getSize(new Vector3()) || new Vector3(1, 1, 1);
        collider.center = bb!.getCenter(new Vector3()) || new Vector3(0, 0, 0);
        addComponent(obj, collider);

        if (opts?.rigidbody === true) {
            addComponent(obj, Rigidbody, { isKinematic: false });
        }
        return collider;
    }

    @validate()
    @serializable(Vector3)
    size: Vector3 = new Vector3(1, 1, 1);

    @serializable(Vector3)
    center: Vector3 = new Vector3(0, 0, 0);

    onEnable() {
        super.onEnable();
        this.context.physics.engine?.addBoxCollider(this, this.size);
        watchWrite(this.gameObject.scale, this.updateProperties);
    }

    onDisable(): void {
        super.onDisable();
        unwatchWrite(this.gameObject.scale, this.updateProperties);
    }

    onValidate(): void {
        this.updateProperties();
    }
}

/**
 * MeshCollider is a collider that represents a mesh shape.  
 * The mesh collider can be used to create a collider from a mesh.
 * @category Physics
 * @group Components
 */
export class MeshCollider extends Collider {

    /**
     * The mesh that is used for the collider.
     */
    @serializable(Mesh)
    sharedMesh?: Mesh;

    /** When `true` the collider won't have holes or entrances.   
     * If you wan't this mesh collider to be able to *contain* other objects this should be set to `false` */
    @serializable()
    convex: boolean = false;

    onEnable() {
        super.onEnable();
        if (!this.context.physics.engine) return;

        if (!this.sharedMesh?.isMesh) {
            // HACK using the renderer mesh
            if (this.gameObject instanceof Mesh) {
                this.sharedMesh = this.gameObject;
            }
        }

        const LOD = 0;

        if (this.sharedMesh?.isMesh) {
            this.context.physics.engine.addMeshCollider(this, this.sharedMesh, this.convex, getWorldScale(this.gameObject));
            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, getWorldScale(this.gameObject));
                }
            })
        }
        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, getWorldScale(this.gameObject));
                        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, getWorldScale(this.gameObject));
                        }
                    }
                });
            }
        }
    }
}

/**
 * CapsuleCollider is a collider that represents a capsule shape.
 * @category Physics
 * @group Components
 */
export class CapsuleCollider extends Collider {
    @serializable(Vector3)
    center: Vector3 = new Vector3(0, 0, 0);

    @serializable()
    radius: number = .5;
    @serializable()
    height: number = 2;

    onEnable() {
        super.onEnable();
        this.context.physics.engine?.addCapsuleCollider(this, this.height, this.radius);
    }

}