var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Component, InputComponent, InputType, LockAxis, PhysXComponent, property, ViewComponent, } from '@wonderlandengine/api';
import { quat, quat2, vec3 } from 'gl-matrix';
import { ActiveCamera } from '../helpers/active-camera.js';
import { DefaultPlayerControllerInput, } from './player-controller-input.js';
import { EPSILON, FORWARD, ZERO_VEC3 } from '../constants.js';
import { componentError, enumStringKeys } from '../utils/wle.js';
import { TempDualQuat, TempVec3 } from '../internal-constants.js';
import { toRad } from '../utils/math.js';
/* Constants */
const BASE_ROT_SPEED = 25;
const SNAP_LOW_THRESHOLD = 0.2;
const SNAP_HIGH_THRESHOLD = 0.2;
/**
 * The type of rotation to use when turning the player
 */
export var RotationType;
(function (RotationType) {
    RotationType[RotationType["None"] = 0] = "None";
    /**
     * Snap will rotate by a fix amount of degrees in a row.
     *
     * It's the most comfortable for most people.
     */
    RotationType[RotationType["Snap"] = 1] = "Snap";
    /**
     * Smooth rotation over multiple frame.
     *
     * Smooth rotation is the most immersive, but can cause motion sickness.
     */
    RotationType[RotationType["Smooth"] = 2] = "Smooth";
})(RotationType || (RotationType = {}));
/** List of string keys for {@link RotationType}. */
export const RotationTypeNames = enumStringKeys(RotationType);
/**
 * Simpler character controller for smooth locomotin.
 *
 * It needs to have a physx component attached to it. And be on the root of the
 * player hierarchy.
 *
 * @see {@link PlayerControllerInput} for handling input from various input providers
 */
export class PlayerController extends Component {
    static TypeName = 'player-controller';
    static onRegister(engine) {
        engine.registerComponent(ActiveCamera);
        engine.registerComponent(DefaultPlayerControllerInput);
    }
    /** Tracked space onto which the rotation is applied */
    trackedSpace;
    /**
     * Object containing the {@link PlayerControllerInput} to read inputs from.
     *
     * @note If not provided, this component will create a default one.
     */
    inputObject = null;
    /* Locomotion properties */
    /** Walk speed multiplier. */
    walkSpeed = 1;
    /* Rotation properties */
    transformType = RotationType.Snap;
    /**
     * The number of degrees at which the player rotates when snap-rotating
     */
    snapDegrees = 45;
    /**
     * Rotation speed multiplier.
     *
     * @note Only used when {@link transformType} is {@link RotationType.Smooth}.
     */
    rotationSpeed = 1;
    /**
     * Input to feed the controller.
     *
     * Using a non-component input is valid, e.g.,
     *
     * ```ts
     * import {PlayerControllerInput} fromn '@wonderlandengine/interaction';
     *
     * class Input implements PlayerControllerInput {
     *     getRotationAxis(out: vec3) {
     *         out[0] = 1.0;
     *     }
     * }
     *
     * controller.input = new Input();
     * ```
     */
    input;
    _activeCamera;
    _physx;
    _snapped = false;
    /** @override */
    start() {
        let maybeCamera = this.object.getComponent(ActiveCamera);
        let maybeInput = (this.inputObject ?? this.object).getComponent(DefaultPlayerControllerInput);
        let left = null;
        let right = null;
        let eyeLeft = null;
        let eyeRight = null;
        if (!maybeCamera || !maybeInput) {
            const inputs = this.scene.getComponents(InputComponent);
            for (const input of inputs) {
                switch (input.inputType) {
                    case InputType.ControllerLeft:
                        left = input;
                        break;
                    case InputType.ControllerRight:
                        right = input;
                        break;
                    case InputType.EyeLeft:
                        eyeLeft = input;
                        break;
                    case InputType.EyeRight:
                        eyeRight = input;
                        break;
                }
            }
        }
        if (!maybeCamera) {
            /* No ActiveCamera found, try our best to create one automatically */
            const views = this.scene.getComponents(ViewComponent);
            let mainView = null;
            for (const view of views) {
                if (view.object.getComponent(InputComponent))
                    continue;
                mainView = view;
                break;
            }
            if (!eyeLeft) {
                throw new Error(componentError(this, "'ActiveCamera' component not provided, and left eye couldn't be found"));
            }
            if (!eyeRight) {
                throw new Error(componentError(this, "'ActiveCamera' component not provided, and right eye couldn't be found"));
            }
            if (!mainView) {
                throw new Error(componentError(this, "'ActiveCamera' component not provided, and non VR view couldn't be found"));
            }
            maybeCamera = this.object.addComponent(ActiveCamera, {
                nonVrCamera: mainView.object,
                leftEye: eyeLeft.object,
                rightEye: eyeRight.object,
            });
        }
        this._activeCamera = maybeCamera;
        if (!maybeInput) {
            /* No input bridge found, try our best to create one automatically */
            if (!left) {
                throw new Error(componentError(this, "'PlayerControllerInput' component not provided, and left controller couldn't be found"));
            }
            if (!right) {
                throw new Error(componentError(this, "'PlayerControllerInput' component not provided, and right controller couldn't be found"));
            }
            maybeInput = this.object.addComponent(DefaultPlayerControllerInput, {
                leftControlObject: left.object,
                rightControlObject: right.object,
            });
        }
        this.input = maybeInput;
    }
    /** @override */
    onActivate() {
        const physx = this.object.getComponent(PhysXComponent);
        if (!physx) {
            throw new Error(componentError(this, 'Missing required physx component'));
        }
        this._physx = physx;
        this._physx.angularLockAxis = LockAxis.X | LockAxis.Y | LockAxis.Z;
        this._snapped = false;
    }
    /** @override */
    update(dt) {
        /* Rotation update */
        const inputRotation = TempVec3.get();
        this.input.getRotationAxis(inputRotation);
        let rotation = 0.0;
        switch (this.transformType) {
            case RotationType.Snap: {
                const value = inputRotation[0];
                if (Math.abs(value) < SNAP_LOW_THRESHOLD) {
                    this._snapped = false;
                    break;
                }
                /* Need to release trigger before snapping again */
                if (this._snapped || Math.abs(value) < SNAP_HIGH_THRESHOLD) {
                    break;
                }
                this._snapped = true;
                rotation = value < 0 ? -this.snapDegrees : this.snapDegrees;
                break;
            }
            case RotationType.Smooth: {
                const value = inputRotation[0];
                if (Math.abs(value) > EPSILON) {
                    rotation = value * this.rotationSpeed * dt * BASE_ROT_SPEED;
                }
                break;
            }
        }
        if (Math.abs(rotation) > EPSILON) {
            this.rotate(-rotation);
        }
        /* Position update */
        const movement = TempVec3.get();
        this.input.getMovementAxis(movement);
        if (!vec3.equals(movement, ZERO_VEC3)) {
            this.move(movement);
        }
        TempVec3.free(2);
    }
    /**
     * Moves the player in the given direction.
     * @param movement The direction to move in.
     */
    move(movement) {
        const currentCamera = this._activeCamera.current;
        const direction = TempVec3.get();
        vec3.transformQuat(direction, movement, currentCamera.getTransformWorld());
        direction[1] = 0;
        vec3.normalize(direction, direction);
        vec3.scale(direction, direction, this.walkSpeed);
        this._physx.linearVelocity = direction;
        TempVec3.free();
    }
    rotate(angle) {
        /* Retrieve eye world transform.
         *
         * We do not directly read it from the active camera in order
         * to remove any tilt rotation from the VR headset.
         */
        const source = TempDualQuat.get();
        const forward = this._activeCamera.getForwardWorld(TempVec3.get());
        forward[1] = 0;
        vec3.normalize(forward, forward);
        quat.rotationTo(source, FORWARD, forward);
        quat2.fromRotationTranslation(source, source, this._activeCamera.getPositionWorld(forward));
        /* Eye target world transform */
        const target = quat2.rotateY(TempDualQuat.get(), source, toRad(angle));
        /* World space delta between source and target */
        const inverse = quat2.invert(source, source);
        const delta = quat2.multiply(target, target, inverse);
        quat2.normalize(delta, delta);
        /* Apply delta to parent tracked space */
        const transform = this.trackedSpace.getTransformWorld(TempDualQuat.get());
        quat2.multiply(transform, delta, transform);
        quat2.normalize(transform, transform);
        this.trackedSpace.setTransformWorld(transform);
        TempDualQuat.free(3);
        TempVec3.free();
    }
}
__decorate([
    property.object({ required: true })
], PlayerController.prototype, "trackedSpace", void 0);
__decorate([
    property.object()
], PlayerController.prototype, "inputObject", void 0);
__decorate([
    property.float(1)
], PlayerController.prototype, "walkSpeed", void 0);
__decorate([
    property.enum(RotationTypeNames, RotationType.Snap)
], PlayerController.prototype, "transformType", void 0);
__decorate([
    property.float(45)
], PlayerController.prototype, "snapDegrees", void 0);
__decorate([
    property.float(1)
], PlayerController.prototype, "rotationSpeed", void 0);
