import { GeospatialCameraInputsManager } from "./geospatialCameraInputsManager.js";
import { Vector3, Matrix, TmpVectors } from "../Maths/math.vector.js";
import { Epsilon } from "../Maths/math.constants.js";
import { Camera } from "./camera.js";
import { GeospatialLimits } from "./Limits/geospatialLimits.js";
import { ComputeLocalBasisToRefs, GeospatialCameraMovement } from "./geospatialCameraMovement.js";
import { Vector3CopyToRef, Vector3Dot } from "../Maths/math.vector.functions.js";
import { Clamp } from "../Maths/math.scalar.functions.js";
import { InterpolatingBehavior } from "../Behaviors/Cameras/interpolatingBehavior.js";
/**
 * @experimental
 * This camera's movements are limited to a camera orbiting a globe, and as the API evolves it will introduce conversions between cartesian coordinates and true lat/long/alt
 *
 * Please note this is marked as experimental and the API (including the constructor!) will change until we remove that flag
 */
export class GeospatialCamera extends Camera {
    constructor(name, scene, options, pickPredicate) {
        super(name, new Vector3(), scene);
        // Temp vars
        this._tempPosition = new Vector3();
        this._viewMatrix = new Matrix();
        this._lookAtVector = new Vector3();
        this._flyToTargets = new Map();
        this._center = new Vector3();
        this._yaw = 0;
        this._pitch = 0;
        this._radius = 0;
        this._tempVect = new Vector3();
        this._tempEast = new Vector3();
        this._tempNorth = new Vector3();
        this._tempUp = new Vector3();
        this._limits = new GeospatialLimits(options.planetRadius);
        this._resetToDefault(this._limits);
        this._flyingBehavior = new InterpolatingBehavior();
        this.addBehavior(this._flyingBehavior);
        this.movement = new GeospatialCameraMovement(scene, this._limits, this.position, this.center, this._lookAtVector, pickPredicate);
        this.pickPredicate = pickPredicate;
        this.inputs = new GeospatialCameraInputsManager(this);
        this.inputs.addMouse().addMouseWheel().addKeyboard();
    }
    /** The point on the globe that we are anchoring around. If no alternate rotation point is supplied, this will represent the center of screen*/
    get center() {
        return this._center;
    }
    /**
     * Sets the camera position to orbit around a new center point
     * @param center The world position (ECEF) to orbit around
     */
    set center(center) {
        this._center.copyFromFloats(center.x, center.y, center.z);
        this._setOrientation(this._yaw, this._pitch, this._radius, this._center);
    }
    /**
     * Gets the camera's yaw (rotation around the geocentric normal) in radians
     */
    get yaw() {
        return this._yaw;
    }
    /**
     * Sets the camera's yaw (rotation around the geocentric normal)
     * @param yaw The desired yaw angle in radians (0 = north, π/2 = east)
     */
    set yaw(yaw) {
        this._setOrientation(yaw, this.pitch, this.radius, this.center);
    }
    /**
     * Gets the camera's pitch (angle from looking straight at globe)
     * Pitch is measured from looking straight down at planet center:
     * - zero pitch = looking straight at planet center (down)
     * - positive pitch = tilting up away from planet
     * - π/2 pitch = looking at horizon (perpendicular to geocentric normal)
     */
    get pitch() {
        return this._pitch;
    }
    /**
     * Sets the camera's pitch (angle from looking straight at globe)
     * @param pitch The desired pitch angle in radians (0 = looking at planet center, π/2 = looking at horizon)
     */
    set pitch(pitch) {
        this._setOrientation(this.yaw, pitch, this.radius, this.center);
    }
    get radius() {
        return this._radius;
    }
    /**
     * Sets the camera's distance from the current center point
     * @param radius The desired radius
     */
    set radius(radius) {
        this._setOrientation(this.yaw, this.pitch, radius, this.center);
    }
    _checkLimits() {
        const limits = this.limits;
        this._yaw = Clamp(this._yaw, limits.yawMin, limits.yawMax);
        this._pitch = Clamp(this._pitch, limits.pitchMin, limits.pitchMax);
        this._radius = Clamp(this._radius, limits.radiusMin, limits.radiusMax);
    }
    _setOrientation(yaw, pitch, radius, center) {
        this._yaw = yaw;
        this._pitch = pitch;
        this._radius = radius;
        Vector3CopyToRef(center, this._center);
        // Clamp to limits
        this._checkLimits();
        // Refresh local basis at center (treat these as read-only for the whole call)
        ComputeLocalBasisToRefs(this._center, this._tempEast, this._tempNorth, this._tempUp);
        // Trig
        const yawScale = this._scene.useRightHandedSystem ? 1 : -1;
        const cosYaw = Math.cos(this._yaw * yawScale);
        const sinYaw = Math.sin(this._yaw * yawScale);
        const sinPitch = Math.sin(this._pitch); // horizontal weight
        const cosPitch = Math.cos(this._pitch); // vertical weight (toward center)
        // Temps
        const horiz = TmpVectors.Vector3[0];
        const t1 = TmpVectors.Vector3[1];
        const t2 = TmpVectors.Vector3[2];
        const right = TmpVectors.Vector3[3];
        // horizontalDirection = North*cosYaw + East*sinYaw  (avoids mutating _temp basis vectors)
        horiz.copyFrom(this._tempNorth).scaleInPlace(cosYaw).addInPlace(t1.copyFrom(this._tempEast).scaleInPlace(sinYaw));
        // look = horiz*sinPitch - Up*cosPitch
        this._lookAtVector.copyFrom(horiz).scaleInPlace(sinPitch).addInPlace(t2.copyFrom(this._tempUp).scaleInPlace(-cosPitch)).normalize(); // keep it unit
        // Build an orthonormal up aligned with geocentric Up
        // right = normalize(cross(upRef, look))
        Vector3.CrossToRef(this._tempUp, this._lookAtVector, right);
        // up = normalize(cross(look, right))
        Vector3.CrossToRef(this._lookAtVector, right, this.upVector);
        // Position = center - look * radius  (preserve unit look)
        this._tempVect.copyFrom(this._lookAtVector).scaleInPlace(-this._radius);
        this._tempPosition.copyFrom(this._center).addInPlace(this._tempVect);
        this._position.copyFrom(this._tempPosition);
        this._isViewMatrixDirty = true;
    }
    /** The point around which the camera will geocentrically rotate. Uses center (pt we are anchored to) if no alternateRotationPt is defined */
    get _geocentricRotationPt() {
        return this.movement.alternateRotationPt ?? this.center;
    }
    /**
     * If camera is actively in flight, will update the target properties and use up the remaining duration from original flyTo call
     *
     * To start a new flyTo curve entirely, call into flyToAsync again (it will stop the inflight animation)
     * @param targetYaw
     * @param targetPitch
     * @param targetRadius
     * @param targetCenter
     */
    updateFlyToDestination(targetYaw, targetPitch, targetRadius, targetCenter) {
        this._flyToTargets.clear();
        this._flyToTargets.set("yaw", targetYaw);
        this._flyToTargets.set("pitch", targetPitch);
        this._flyToTargets.set("radius", targetRadius);
        this._flyToTargets.set("center", targetCenter);
        this._flyingBehavior.updateProperties(this._flyToTargets);
    }
    /**
     * Animate camera towards passed in property values. If undefined, will use current value
     * @param targetYaw
     * @param targetPitch
     * @param targetRadius
     * @param targetCenter
     * @param flightDurationMs
     * @param easingFunction
     * @returns Promise that will return when the animation is complete (or interuppted by pointer input)
     */
    async flyToAsync(targetYaw, targetPitch, targetRadius, targetCenter, flightDurationMs = 1000, easingFunction) {
        this._flyToTargets.clear();
        this._flyToTargets.set("yaw", targetYaw);
        this._flyToTargets.set("pitch", targetPitch);
        this._flyToTargets.set("radius", targetRadius);
        this._flyToTargets.set("center", targetCenter);
        return await this._flyingBehavior.animatePropertiesAsync(this._flyToTargets, flightDurationMs, easingFunction);
    }
    get limits() {
        return this._limits;
    }
    _resetToDefault(limits) {
        // Camera configuration vars
        const maxCameraRadius = limits.altitudeMax !== undefined ? limits.planetRadius + limits.altitudeMax : undefined;
        const restingAltitude = maxCameraRadius ?? limits.planetRadius * 4;
        this.position.copyFromFloats(restingAltitude, 0, 0);
        this._center.copyFromFloats(limits.planetRadius, 0, 0);
        this._radius = Vector3.Distance(this.position, this.center);
        // Temp vars
        this._tempPosition = new Vector3();
        // View matrix calculation vars
        this._viewMatrix = Matrix.Identity();
        this._center.subtractToRef(this._position, this._lookAtVector).normalize(); // Lookat vector of the camera
        this.upVector = Vector3.Up(); // Up vector of the camera (does work for -X look at)
        this._isViewMatrixDirty = true;
        this._setOrientation(this._yaw, this._pitch, this._radius, this._center);
    }
    /** @internal */
    _getViewMatrix() {
        if (!this._isViewMatrixDirty) {
            return this._viewMatrix;
        }
        this._isViewMatrixDirty = false;
        // Ensure vectors are normalized
        this.upVector.normalize();
        this._lookAtVector.normalize();
        // Calculate view matrix with camera position and center
        if (this.getScene().useRightHandedSystem) {
            Matrix.LookAtRHToRef(this.position, this._center, this.upVector, this._viewMatrix);
        }
        else {
            Matrix.LookAtLHToRef(this.position, this._center, this.upVector, this._viewMatrix);
        }
        return this._viewMatrix;
    }
    /** @internal */
    _isSynchronizedViewMatrix() {
        if (!super._isSynchronizedViewMatrix() || this._isViewMatrixDirty) {
            return false;
        }
        return true;
    }
    _applyGeocentricTranslation() {
        // Store pending position (without any corrections applied)
        this.center.addToRef(this.movement.panDeltaCurrentFrame, this._tempPosition);
        if (!this.movement.isInterpolating) {
            // Calculate the position correction to keep camera at the same radius when applying translation
            this._tempPosition.normalize().scaleInPlace(this.center.length());
        }
        // Set center which will call _setOrientation
        this.center = this._tempPosition;
    }
    /**
     * This rotation keeps the camera oriented towards the globe as it orbits around it. This is different from cameraCentricRotation which is when the camera rotates around its own axis
     */
    _applyGeocentricRotation() {
        const rotationDeltaCurrentFrame = this.movement.rotationDeltaCurrentFrame;
        if (rotationDeltaCurrentFrame.x !== 0 || rotationDeltaCurrentFrame.y !== 0) {
            const pitch = rotationDeltaCurrentFrame.x !== 0 ? Clamp(this._pitch + rotationDeltaCurrentFrame.x, 0, 0.5 * Math.PI - Epsilon) : this._pitch;
            const yaw = rotationDeltaCurrentFrame.y !== 0 ? this._yaw + rotationDeltaCurrentFrame.y : this._yaw;
            // TODO: If _geocentricRotationPt is not the center, this will need to be adjusted.
            this._setOrientation(yaw, pitch, this._radius, this._geocentricRotationPt);
        }
    }
    _applyZoom(zoomVector, distance) {
        // TODO this function will be re-worked shortly after checkin, becuase today it breaks down if you zoom to a point past the center
        // (ex: tilted view zooming towards cursor near horizon where the center is closer than the cursor point).
        // Project zoom vector onto lookAt vector to find the amount the camera-to-center distance should change.
        // - zoom vector is normalized
        // - distance is how much to move in this call
        const directionDotLookAt = Vector3Dot(zoomVector, this._lookAtVector);
        const hasRadialComponent = Math.abs(directionDotLookAt) > Epsilon;
        const requestedRadius = hasRadialComponent ? this._radius - distance * directionDotLookAt : this._radius;
        const newRadius = Clamp(requestedRadius, this.limits.radiusMin, this.limits.radiusMax);
        const actualRadiusChange = newRadius - this._radius;
        const actualDistanceChange = hasRadialComponent ? actualRadiusChange / directionDotLookAt : 0;
        // Use this to compute new camera position and new center position.
        const newCameraPosition = this._position.add(zoomVector.scale(-actualDistanceChange));
        const newCenter = newCameraPosition.add(this._lookAtVector.scaleToRef(newRadius, TmpVectors.Vector3[3]));
        // Rescale new center to maintain same altitude as the old center.
        const currentCenterRadius = this._center.length();
        const newCenterRadius = newCenter.length();
        const newCenterRescale = currentCenterRadius / newCenterRadius;
        newCenter.scaleInPlace(newCenterRescale);
        // Apply changes
        this._setOrientation(this._yaw, this._pitch, newRadius, newCenter);
    }
    _checkInputs() {
        this.inputs.checkInputs();
        // Let movement class handle all per-frame logic
        this.movement.computeCurrentFrameDeltas();
        if (this.movement.panDeltaCurrentFrame.lengthSquared() > 0) {
            this._applyGeocentricTranslation();
            this._isViewMatrixDirty = true;
        }
        if (this.movement.rotationDeltaCurrentFrame.lengthSquared() > 0) {
            this._applyGeocentricRotation();
            this._isViewMatrixDirty = true;
        }
        if (Math.abs(this.movement.zoomDeltaCurrentFrame) > Epsilon) {
            this._applyZoom(this.movement.computedPerFrameZoomVector, this.movement.zoomDeltaCurrentFrame);
            this._isViewMatrixDirty = true;
        }
        super._checkInputs();
    }
    attachControl(noPreventDefault) {
        this.inputs.attachElement(noPreventDefault);
    }
    detachControl() {
        this.inputs.detachElement();
    }
}
//# sourceMappingURL=geospatialCamera.js.map