import { observer } from "mobx-react-lite";
import type { Character } from "../types";
import { SyncRigidBody } from "./SyncRigidBody";
import { state } from "../state";
import { useEffect, useRef } from "react";
import { Euler, Quaternion, Vector3 } from "three";
import { useFrame, useThree } from "@react-three/fiber";
import { useKeyboardControls } from "@react-three/drei";
import { useRapier, useBeforePhysicsStep, type RapierRigidBody, type RapierCollider, RoundCuboidCollider } from "@react-three/rapier";
import { joystickValues } from "../joystickInput";

const SPEED = 5;
const JUMP_SPEED = 8;
const GRAVITY = 20;
const SENSITIVITY = 0.002;
const SHOULDER_OFFSET = new Vector3(0.1, 1.5, 2.5);
const LOOK_RATE = 2000;

// recreate private types
type RapierWorldCreateCharacterControllerFunction = ReturnType<typeof useRapier>['world']['createCharacterController'];
type KinematicCharacterController = ReturnType<RapierWorldCreateCharacterControllerFunction>;

const _q = new Quaternion();
const _e = new Euler(0, 0, 0, 'YXZ');
const _offset = new Vector3();
const _charPos = new Vector3();
const _lookAt = new Vector3();

type CharacterViewProps = {
    character: Character;
    editMode?: boolean;
}

export const CharacterView = observer(function ({ character, editMode }: CharacterViewProps) {
    const pos = character.transform.position;
    const rbRef = useRef<RapierRigidBody>(null);
    const colliderRef = useRef<RapierCollider>(null);
    const [, get] = useKeyboardControls();
    const { gl } = useThree();
    const { world } = useRapier();

    const yawRef = useRef(0);
    const pitchRef = useRef(0);
    const mouseRef = useRef({ x: 0, y: 0 });
    const jumpPressedRef = useRef(false);
    const vyRef = useRef(0);
    const controllerRef = useRef<KinematicCharacterController | null>(null);

    useEffect(() => {
        const controller = world.createCharacterController(0.01);
        controller.setUp({ x: 0, y: 1, z: 0 });
        controller.setMaxSlopeClimbAngle(45 * Math.PI / 180);
        controller.setMinSlopeSlideAngle(30 * Math.PI / 180);
        controller.enableAutostep(0.5, 0.2, true);
        controller.enableSnapToGround(0.5);
        controller.setApplyImpulsesToDynamicBodies(true);
        controllerRef.current = controller;
        return () => { world.removeCharacterController(controller); };
    }, [world]);

    useEffect(() => {
        if (editMode) return;
        const canvas = gl.domElement;
        const onClick = () => canvas.requestPointerLock();
        const onMouseMove = (e: MouseEvent) => {
            if (document.pointerLockElement !== canvas) return;
            mouseRef.current.x += e.movementX;
            mouseRef.current.y += e.movementY;
        };
        canvas.addEventListener('click', onClick);
        document.addEventListener('mousemove', onMouseMove);
        return () => {
            canvas.removeEventListener('click', onClick);
            document.removeEventListener('mousemove', onMouseMove);
        };
    }, [gl, editMode]);

    useBeforePhysicsStep((world) => {
        if (editMode) return;
        const rb = rbRef.current;
        const collider = colliderRef.current;
        const controller = controllerRef.current;
        if (!rb || !collider || !controller) return;
        if (state.game?.isPaused) return;

        const dt = world.timestep;
        const yaw = yawRef.current;
        const { forward, backward, left, right, jump } = get();
        const fwdX = -Math.sin(yaw);
        const fwdZ = -Math.cos(yaw);
        const fwdScale = Math.max(-1, Math.min(1, (forward ? 1 : 0) - (backward ? 1 : 0) + joystickValues.move.y));
        const rightScale = Math.max(-1, Math.min(1, (right ? 1 : 0) - (left ? 1 : 0) + joystickValues.move.x));
        const vx = (fwdX * fwdScale - fwdZ * rightScale) * SPEED;
        const vz = (fwdZ * fwdScale + fwdX * rightScale) * SPEED;

        const jumpInput = jump || joystickValues.jump;
        const isGrounded = controller.computedGrounded();
        if (jumpInput && !jumpPressedRef.current && isGrounded) {
            vyRef.current = JUMP_SPEED;
        } else if (!isGrounded) {
            vyRef.current -= GRAVITY * dt;
        } else {
            vyRef.current = 0;
        }
        jumpPressedRef.current = jumpInput;

        controller.computeColliderMovement(collider, { x: vx * dt, y: vyRef.current * dt, z: vz * dt });
        const corrected = controller.computedMovement();
        const t = rb.translation();
        rb.setNextKinematicTranslation({
            x: t.x + corrected.x,
            y: t.y + corrected.y,
            z: t.z + corrected.z,
        });

        _q.setFromEuler(_e.set(0, yaw, 0));
        rb.setNextKinematicRotation({ x: _q.x, y: _q.y, z: _q.z, w: _q.w });
    });

    useFrame(({ camera }, delta) => {
        if (editMode) return;
        const rb = rbRef.current;
        if (!rb) return;

        mouseRef.current.x += joystickValues.look.x * LOOK_RATE * delta;
        mouseRef.current.y += -joystickValues.look.y * LOOK_RATE * delta;

        yawRef.current -= mouseRef.current.x * SENSITIVITY;
        pitchRef.current = Math.max(-0.85, Math.min(0.5, pitchRef.current - mouseRef.current.y * SENSITIVITY));
        mouseRef.current.x = 0;
        mouseRef.current.y = 0;

        const t = rb.translation();
        _charPos.set(t.x, t.y, t.z);
        _offset.copy(SHOULDER_OFFSET).applyEuler(_e.set(pitchRef.current, yawRef.current, 0, 'YXZ'));
        camera.position.copy(_charPos).add(_offset);
        _lookAt.set(t.x, t.y + 1, t.z);
        camera.lookAt(_lookAt);
    });

    return (
        <SyncRigidBody
            ref={rbRef}
            type="kinematicPosition"
            colliders={false}
            position={[pos[0] + 0.5, pos[1] + 0.5, pos[2] + 0.5]}
            onSync={() => { }}
        >
            {/* <BallCollider ref={colliderRef} args={[0.55]} /> */}
            {/* <CapsuleCollider ref={colliderRef} args={[0.4, 0.5]} /> */}
            {/* <CuboidCollider ref={colliderRef} args={[0.55, 0.4, 0.55]} /> */}
            <RoundCuboidCollider ref={colliderRef} args={[0.4, 0.4, 0.4, 0.1]} />
            <group>
                <mesh rotation={[-Math.PI / 2, -Math.PI / 4, 0]}>
                    <coneGeometry args={[0.55, 0.8, 4]} />
                    <meshStandardMaterial color="yellow" />
                </mesh>
            </group>
        </SyncRigidBody>
    );
});
