import { Quaternion, Ray, Vector2, Vector3 } from "three";

import { Mathf } from "../engine/engine_math.js";
import { RaycastOptions } from "../engine/engine_physics.js";
import { serializable } from "../engine/engine_serialization.js";
import { getWorldPosition } from "../engine/engine_three_utils.js";
import { Collision } from "../engine/engine_types.js";
import { getParam } from "../engine/engine_utils.js";
import { Animator } from "./Animator.js"
import { CapsuleCollider } from "./Collider.js";
import { Behaviour } from "./Component.js";
import { Rigidbody } from "./RigidBody.js";

const debug = getParam("debugcharactercontroller");

/**
 * The [CharacterController](https://engine.needle.tools/docs/api/CharacterController) adds a capsule collider and rigidbody to the object, constrains rotation, and provides movement and grounded state.
 * It is designed for typical character movement in 3D environments.  
 *
 * The controller automatically:
 * - Creates a {@link CapsuleCollider} if one doesn't exist
 * - Creates a {@link Rigidbody} if one doesn't exist
 * - Locks rotation on all axes to prevent tipping over
 * - Tracks ground contact for jump detection
 *
 * @example Basic character movement
 * ```ts
 * export class MyCharacter extends Behaviour {
 *   @serializable(CharacterController)
 *   controller?: CharacterController;
 *
 *   update() {
 *     const input = this.context.input;
 *     const move = new Vector3();
 *     if (input.isKeyPressed("KeyW")) move.z = 0.1;
 *     if (input.isKeyPressed("KeyS")) move.z = -0.1;
 *     this.controller?.move(move);
 *   }
 * }
 * ```
 *
 * @summary Character Movement Controller
 * @category Character
 * @group Components
 * @see {@link CharacterControllerInput} for ready-to-use input handling
 * @see {@link Rigidbody} for physics configuration
 * @see {@link CapsuleCollider} for collision shape
 */
export class CharacterController extends Behaviour {

    /** Center offset of the capsule collider in local space */
    @serializable(Vector3)
    center: Vector3 = new Vector3(0, 0, 0);

    /** Radius of the capsule collider */
    @serializable()
    radius: number = .5;

    /** Height of the capsule collider */
    @serializable()
    height: number = 2;

    private _rigidbody: Rigidbody | null = null;
    get rigidbody(): Rigidbody {
        if (this._rigidbody) return this._rigidbody;
        this._rigidbody = this.gameObject.getComponent(Rigidbody);
        if (!this._rigidbody)
            this._rigidbody = this.gameObject.addComponent(Rigidbody) as Rigidbody;
        return this.rigidbody;
    }

    private _activeGroundCollisions!: Set<Collision>;

    awake(): void {
        this._activeGroundCollisions = new Set<Collision>();
    }

    onEnable() {
        const rb = this.rigidbody;
        let collider = this.gameObject.getComponent(CapsuleCollider);
        if (!collider)
            collider = this.gameObject.addComponent(CapsuleCollider) as CapsuleCollider;

        collider.center.copy(this.center);
        collider.radius = this.radius;
        collider.height = this.height;

        // discard any rotation besides Y axis
        const wForward = new Vector3(0, 0, 1);
        const wRight = new Vector3(1, 0, 0);
        const wUp = new Vector3(0, 1, 0);
        const fwd = this.gameObject.getWorldDirection(new Vector3());
        fwd.y = 0;

        const sign = wRight.dot(fwd) < 0 ? -1 : 1;
        const angleY = wForward.angleTo(fwd) * sign;
        this.gameObject.setRotationFromAxisAngle(wUp, angleY);

        rb.lockRotationX = true;
        rb.lockRotationY = true;
        rb.lockRotationZ = true;
    }

    /**
     * Moves the character by adding the given vector to its position.
     * Movement is applied directly without physics simulation.
     * @param vec The movement vector to apply
     */
    move(vec: Vector3) {
        this.gameObject.position.add(vec);
    }

    onCollisionEnter(col: Collision) {
        // contacts can be empty, ignoring such collision results in a stuck grounded state
        // namely caused by mesh colliders
        if (col.contacts.length == 0 || col.contacts.some(contact => contact.normal.y > 0.2)) {
            this._activeGroundCollisions.add(col);
            if (debug) {
                console.log(`Collision(${this._activeGroundCollisions.size}): ${col.contacts.map(c => c.normal.y.toFixed(2)).join(", ")} - ${this.isGrounded}`);
            }
        }
    }

    onCollisionExit(col: Collision) {
        this._activeGroundCollisions.delete(col);
        if (debug) {
            console.log(`Collision(${this._activeGroundCollisions.size}) - ${this.isGrounded}`);
        }
    }

    /** Returns true if the character is currently touching the ground */
    get isGrounded(): boolean { return this._activeGroundCollisions.size > 0; }

    private _contactVelocity: Vector3 = new Vector3();

    /**
     * Returns the combined velocity of all objects the character is standing on.
     * Useful for moving platforms - add this to your movement for proper platform riding.
     */
    get contactVelocity(): Vector3 {
        this._contactVelocity.set(0, 0, 0);
        for (const col of this._activeGroundCollisions) {
            const vel = this.context.physics.engine?.getLinearVelocity(col.collider);
            if (!vel) continue;
            // const friction = col.collider.sharedMaterial?.dynamicFriction || 1;
            this._contactVelocity.x += vel.x;
            this._contactVelocity.y += vel.y;
            this._contactVelocity.z += vel.z;
        }
        return this._contactVelocity;
    }
}

/**
 * CharacterControllerInput handles user input to control a {@link CharacterController}.
 * It supports movement, looking around, jumping, and double jumping.
 *
 * Default controls:
 * - **W/S**: Move forward/backward
 * - **A/D**: Rotate left/right
 * - **Space**: Jump (supports double jump)
 *
 * The component automatically sets animator parameters:
 * - `running` (bool): True when moving
 * - `jumping` (bool): True when starting a jump
 * - `doubleJump` (bool): True during double jump
 * - `falling` (bool): True when falling from height
 *
 * @example Custom input handling
 * ```ts
 * const input = this.gameObject.getComponent(CharacterControllerInput);
 * input?.move(new Vector2(0, 1)); // Move forward
 * input?.jump(); // Trigger jump
 * ```
 *
 * @summary User Input for Character Controller
 * @category Character
 * @group Components
 * @see {@link CharacterController} for the movement controller
 * @see {@link Animator} for animation integration
 */
export class CharacterControllerInput extends Behaviour {

    /** The CharacterController to drive with input */
    @serializable(CharacterController)
    controller?: CharacterController;

    /** Movement speed multiplier */
    @serializable()
    movementSpeed: number = 2;

    /** Rotation speed multiplier */
    @serializable()
    rotationSpeed: number = 2;

    /** Impulse force applied when jumping from ground */
    @serializable()
    jumpForce: number = 1;

    /** Impulse force applied for the second jump (set to 0 to disable double jump) */
    @serializable()
    doubleJumpForce: number = 2;

    /** Optional Animator for character animations */
    @serializable(Animator)
    animator?: Animator;

    lookForward: boolean = true;

    awake(){
        this._currentRotation = new Quaternion();
    }

    update(){
        const input = this.context.input;
        if(input.isKeyPressed("KeyW"))
            this.moveInput.y += 1;
        else if(input.isKeyPressed("KeyS"))
            this.moveInput.y -= 1;
        if(input.isKeyPressed("KeyD"))
            this.lookInput.x += 1;
        else if(input.isKeyPressed("KeyA"))
            this.lookInput.x -= 1;

        this.jumpInput ||= input.isKeyDown("Space");
    }

    move(move: Vector2) {
        this.moveInput.add(move);
    }

    look(look: Vector2) {
        this.lookInput.add(look);
    }

    jump() {
        this.jumpInput = true;
    }

    private lookInput: Vector2 = new Vector2(0, 0);
    private moveInput: Vector2 = new Vector2(0, 0);
    private jumpInput: boolean = false;

    onBeforeRender() {
        this.handleInput(this.moveInput, this.lookInput, this.jumpInput);

        this.lookInput.set(0, 0);
        this.moveInput.set(0, 0);
        this.jumpInput = false;
    }

    private _currentSpeed: Vector3 = new Vector3(0, 0, 0);
    private _currentAngularSpeed: Vector3 = new Vector3(0, 0, 0);

    private _temp: Vector3 = new Vector3(0, 0, 0);
    private _jumpCount: number = 0;
    private _currentRotation!: Quaternion;

    handleInput (move: Vector2, look: Vector2, jump: boolean) {

        if (this.controller?.isGrounded) {
            this._jumpCount = 0;
            if (this.doubleJumpForce > 0) this.animator?.setBool("doubleJump", false);
        }

        this._currentSpeed.z += move.y * this.movementSpeed * this.context.time.deltaTime;

        this.animator?.setBool("running", move.length() > 0.01);
        this.animator?.setBool("jumping", this.controller?.isGrounded === true && jump);

        this._temp.copy(this._currentSpeed);
        this._temp.applyQuaternion(this.gameObject.quaternion);
        if (this.controller) this.controller.move(this._temp);
        else this.gameObject.position.add(this._temp);

        this._currentAngularSpeed.y += Mathf.toRadians(-look.x * this.rotationSpeed) * this.context.time.deltaTime;
        if (this.lookForward && Math.abs(this._currentAngularSpeed.y) < .01) {
            const forwardVector = this.context.mainCameraComponent!.forward;
            forwardVector.y = 0;
            forwardVector.normalize();
            this._currentRotation.setFromUnitVectors(new Vector3(0, 0, 1), forwardVector);
            this.gameObject.quaternion.slerp(this._currentRotation, this.context.time.deltaTime * 10);
        }
        this.gameObject.rotateY(this._currentAngularSpeed.y);


        this._currentSpeed.multiplyScalar(1 - this.context.time.deltaTime * 10);
        this._currentAngularSpeed.y *= 1 - this.context.time.deltaTime * 10;

        if (this.controller && jump && this.jumpForce > 0) {
            let canJump = this.controller?.isGrounded;
            if (this.doubleJumpForce > 0 && !this.controller?.isGrounded && this._jumpCount === 1) {
                canJump = true;
                this.animator?.setBool("doubleJump", true);
            }

            if (canJump) {
                this._jumpCount += 1;
                // TODO: factor in mass
                const rb = this.controller.rigidbody;
                // const fullJumpHoldLength = .1;
                const factor = this._jumpCount === 2 ? this.doubleJumpForce : this.jumpForce;// Mathf.clamp((this.context.time.time - this._jumpDownTime), 0, fullJumpHoldLength) / fullJumpHoldLength;
                rb.applyImpulse(new Vector3(0, 1, 0).multiplyScalar(factor));
            }
        }

        if (this.controller) {
            // TODO: should probably raycast to the ground or check if we're still in the jump animation
            const verticalSpeed = this.controller?.rigidbody.getVelocity().y;
            if (verticalSpeed < -1) {
                if (!this._raycastOptions.ray) this._raycastOptions.ray = new Ray();
                this._raycastOptions.ray.origin.copy(getWorldPosition(this.gameObject));
                this._raycastOptions.ray.direction.set(0, -1, 0);
                const currentLayer = this.layer;
                this.gameObject.layers.disableAll();
                this.gameObject.layers.set(2);
                const hits = this.context.physics.raycast(this._raycastOptions);
                this.gameObject.layers.set(currentLayer);
                if ((hits.length && hits[0].distance > 2 || verticalSpeed < -10)) {
                    this.animator?.setBool("falling", true);
                }
            }
            else this.animator?.setBool("falling", false);
        }
    }

    private _raycastOptions = new RaycastOptions();
}