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 } from '@wonderlandengine/api';
import { property } from '@wonderlandengine/api/decorators.js';
import { deg2rad, rad2deg } from './utils/utils.js';
import { quat, vec3 } from 'gl-matrix';
const preventDefault = (e) => {
    e.preventDefault();
};
const tempVec = [0, 0, 0];
const tempquat = quat.create();
const tempquat2 = quat.create();
const tempVec3 = vec3.create();
/**
 * OrbitalCamera component allows the user to orbit around a target point, which
 * is the position of the object itself. It rotates at the specified distance.
 *
 * @remarks
 * The component works using mouse or touch. Therefor it does not work in VR.
 */
class OrbitalCamera extends Component {
    static TypeName = 'orbital-camera';
    mouseButtonIndex = 0;
    radial = 5;
    minElevation = 0.0;
    maxElevation = 89.99;
    minZoom = 0.01;
    maxZoom = 10.0;
    xSensitivity = 0.5;
    ySensitivity = 0.5;
    zoomSensitivity = 0.02;
    damping = 0.9;
    target = null;
    _mouseDown = false;
    _origin = vec3.create();
    _originCache = vec3.create();
    _azimuth = 0;
    _polar = 45;
    _initialPinchDistance = 0;
    _touchStartX = 0;
    _touchStartY = 0;
    _azimuthSpeed = 0;
    _polarSpeed = 0;
    init() {
        if (this.target) {
            this.target.getPositionWorld(this._origin);
        }
        else {
            this.object.getPositionWorld(this._origin);
        }
    }
    start() {
        this._polar = Math.min(this.maxElevation, Math.max(this.minElevation, this._polar));
        this._updateCamera();
    }
    onActivate() {
        const canvas = this.engine.canvas;
        if (this.mouseButtonIndex === 2) {
            canvas.addEventListener('contextmenu', preventDefault, { passive: false });
        }
        canvas.addEventListener('mousedown', this._onMouseDown);
        canvas.addEventListener('wheel', this._onMouseScroll, { passive: false });
        canvas.addEventListener('touchstart', this._onTouchStart, { passive: false });
        canvas.addEventListener('touchmove', this._onTouchMove, { passive: false });
        canvas.addEventListener('touchend', this._onTouchEnd);
    }
    onDeactivate() {
        const canvas = this.engine.canvas;
        if (this.mouseButtonIndex === 2) {
            canvas.removeEventListener('contextmenu', preventDefault);
        }
        canvas.removeEventListener('mousemove', this._onMouseMove);
        canvas.removeEventListener('mousedown', this._onMouseDown);
        canvas.removeEventListener('wheel', this._onMouseScroll);
        canvas.removeEventListener('touchstart', this._onTouchStart);
        canvas.removeEventListener('touchmove', this._onTouchMove);
        canvas.removeEventListener('touchend', this._onTouchEnd);
        /* Reset state to make sure nothing gets stuck */
        this._mouseDown = false;
        this._initialPinchDistance = 0;
        this._touchStartX = 0;
        this._touchStartY = 0;
        this._azimuthSpeed = 0;
        this._polarSpeed = 0;
    }
    update() {
        // Update the orbit center to the target's position if one is assigned.
        if (this.target) {
            this.target.getPositionWorld(this._origin);
        }
        /* Always apply damping, because there's no event for stop moving */
        this._azimuthSpeed *= this.damping;
        this._polarSpeed *= this.damping;
        /* Stop completely if the speed is very low to avoid endless tiny movements */
        if (Math.abs(this._azimuthSpeed) < 0.01)
            this._azimuthSpeed = 0;
        if (Math.abs(this._polarSpeed) < 0.01)
            this._polarSpeed = 0;
        /* Apply the speed to the camera angles */
        this._azimuth += this._azimuthSpeed;
        this._polar += this._polarSpeed;
        /* Clamp the polar angle */
        this._polar = Math.min(this.maxElevation, Math.max(this.minElevation, this._polar));
        /* Update the camera if there's any speed */
        if (this._azimuthSpeed !== 0 ||
            this._polarSpeed !== 0 ||
            vec3.equals(this._origin, this._originCache) === false) {
            this._updateCamera();
            vec3.copy(this._originCache, this._origin);
        }
    }
    /**
     * Get the closest position to the given position on the orbit sphere of the camera.
     * This can be used to get a position and rotation to transition to.
     *
     * You pass this a position object. The method calculates the closest positition and updates the position parameter.
     * It also sets the rotation parameter to reflect the rotate the camera will have when it is on the orbit sphere,
     * pointing towards the center.
     * @param position the position to get the closest position to
     * @param rotation the rotation to get the closest position to
     */
    getClosestPosition(position, rotation) {
        /* It's a bit hacky, but the easiest way to get the rotation of the camera is just briefly
         change the rotation to look at the center and then get the rotation. */
        this.object.getRotationWorld(tempquat);
        this.object.lookAt(this._origin);
        this.object.getRotationWorld(tempquat2);
        if (quat.dot(tempquat, tempquat2) < 0) {
            quat.scale(tempquat2, tempquat2, -1); /* Negate to ensure shortest path */
        }
        this.object.setRotationWorld(tempquat);
        /* Calculate the direction from the center of orbit to the current camera position */
        const directionToCamera = vec3.create();
        vec3.subtract(directionToCamera, position, this._origin);
        vec3.normalize(directionToCamera, directionToCamera);
        /* Scale this direction by the radius of your orbital sphere to get the nearest point on the sphere */
        const nearestPointOnSphere = vec3.create();
        vec3.scale(nearestPointOnSphere, directionToCamera, this.radial);
        vec3.add(nearestPointOnSphere, nearestPointOnSphere, this._origin);
        vec3.copy(position, nearestPointOnSphere);
        quat.copy(rotation, tempquat2);
    }
    /**
     * Set the camera position based on the given position and calculate the rotation.
     * @param cameraPosition the position to set the camera to
     */
    setPosition(cameraPosition) {
        const centerOfOrbit = this._origin;
        /* Compute the direction vector */
        vec3.subtract(tempVec3, cameraPosition, centerOfOrbit);
        vec3.normalize(tempVec3, tempVec3);
        /* Compute the azimuth angle (in radians) */
        const azimuth = Math.atan2(tempVec3[0], tempVec3[2]);
        /* Compute the polar angle (in radians) */
        const polar = Math.acos(tempVec3[1]);
        const azimuthDeg = rad2deg(azimuth);
        /* Polar is inverted to match the orbital camera */
        const polarDeg = 90 - rad2deg(polar);
        this._azimuth = azimuthDeg;
        this._polar = polarDeg;
    }
    /**
     * Update the camera position based on the current azimuth,
     * polar and radial values
     */
    _updateCamera() {
        const azimuthInRadians = deg2rad(this._azimuth);
        const polarInRadians = deg2rad(this._polar);
        tempVec[0] = this.radial * Math.sin(azimuthInRadians) * Math.cos(polarInRadians);
        tempVec[1] = this.radial * Math.sin(polarInRadians);
        tempVec[2] = this.radial * Math.cos(azimuthInRadians) * Math.cos(polarInRadians);
        this.object.setPositionWorld(tempVec);
        this.object.translateWorld(this._origin);
        this.object.lookAt(this._origin);
    }
    /* Mouse Event Handlers */
    _onMouseDown = (e) => {
        window.addEventListener('mouseup', this._onMouseUp);
        window.addEventListener('mousemove', this._onMouseMove);
        if (e.button === this.mouseButtonIndex) {
            this._mouseDown = true;
            document.body.style.cursor = 'grabbing';
            if (e.button === 1) {
                e.preventDefault(); /* to prevent scrolling */
                return false;
            }
        }
    };
    _onMouseUp = (e) => {
        window.removeEventListener('mouseup', this._onMouseUp);
        window.removeEventListener('mousemove', this._onMouseMove);
        if (e.button === this.mouseButtonIndex) {
            this._mouseDown = false;
            document.body.style.cursor = 'initial';
        }
    };
    _onMouseMove = (e) => {
        if (this.active && this._mouseDown) {
            if (this.active && this._mouseDown) {
                this._azimuthSpeed = -(e.movementX * this.xSensitivity);
                this._polarSpeed = e.movementY * this.ySensitivity;
            }
        }
    };
    _onMouseScroll = (e) => {
        e.preventDefault(); /* to prevent scrolling */
        this.radial *= 1 - e.deltaY * this.zoomSensitivity * -0.001;
        this.radial = Math.min(this.maxZoom, Math.max(this.minZoom, this.radial));
        this._updateCamera();
    };
    /* Touch event handlers */
    _onTouchStart = (e) => {
        if (e.touches.length === 1) {
            /* to prevent scrolling and allow us to track touch movement */
            e.preventDefault();
            this._touchStartX = e.touches[0].clientX;
            this._touchStartY = e.touches[0].clientY;
            this._mouseDown = true; /* Treat touch like mouse down */
        }
        else if (e.touches.length === 2) {
            /* Calculate initial pinch distance */
            this._initialPinchDistance = this._getDistanceBetweenTouches(e.touches);
            e.preventDefault(); /* Prevent default pinch actions */
        }
    };
    _onTouchMove = (e) => {
        if (!this.active || !this._mouseDown) {
            return;
        }
        e.preventDefault(); /* to prevent moving the page */
        if (e.touches.length === 1) {
            const deltaX = e.touches[0].clientX - this._touchStartX;
            const deltaY = e.touches[0].clientY - this._touchStartY;
            this._azimuthSpeed = -(deltaX * this.xSensitivity);
            this._polarSpeed = deltaY * this.ySensitivity;
            this._touchStartX = e.touches[0].clientX;
            this._touchStartY = e.touches[0].clientY;
        }
        else if (e.touches.length === 2) {
            /* Handle pinch zoom */
            const currentPinchDistance = this._getDistanceBetweenTouches(e.touches);
            const pinchScale = this._initialPinchDistance / currentPinchDistance;
            this.radial *= pinchScale;
            this.radial = Math.min(this.maxZoom, Math.max(this.minZoom, this.radial));
            this._updateCamera();
            /* Update initial pinch distance for next move */
            this._initialPinchDistance = currentPinchDistance;
        }
    };
    _onTouchEnd = (e) => {
        if (e.touches.length < 2) {
            this._mouseDown = false; /* Treat touch end like mouse up */
        }
        if (e.touches.length === 1) {
            /* Prepare for possible single touch movement */
            this._touchStartX = e.touches[0].clientX;
            this._touchStartY = e.touches[0].clientY;
        }
    };
    /**
     * Helper function to calculate the distance between two touch points
     * @param touches list of touch points
     * @returns distance between the two touch points
     */
    _getDistanceBetweenTouches(touches) {
        const dx = touches[0].clientX - touches[1].clientX;
        const dy = touches[0].clientY - touches[1].clientY;
        return Math.sqrt(dx * dx + dy * dy);
    }
}
__decorate([
    property.int()
], OrbitalCamera.prototype, "mouseButtonIndex", void 0);
__decorate([
    property.float(5)
], OrbitalCamera.prototype, "radial", void 0);
__decorate([
    property.float()
], OrbitalCamera.prototype, "minElevation", void 0);
__decorate([
    property.float(89.99)
], OrbitalCamera.prototype, "maxElevation", void 0);
__decorate([
    property.float()
], OrbitalCamera.prototype, "minZoom", void 0);
__decorate([
    property.float(10.0)
], OrbitalCamera.prototype, "maxZoom", void 0);
__decorate([
    property.float(0.5)
], OrbitalCamera.prototype, "xSensitivity", void 0);
__decorate([
    property.float(0.5)
], OrbitalCamera.prototype, "ySensitivity", void 0);
__decorate([
    property.float(0.02)
], OrbitalCamera.prototype, "zoomSensitivity", void 0);
__decorate([
    property.float(0.9)
], OrbitalCamera.prototype, "damping", void 0);
__decorate([
    property.object()
], OrbitalCamera.prototype, "target", void 0);
export { OrbitalCamera };
//# sourceMappingURL=orbital-camera.js.map