import { CameraMovement } from "./cameraMovement.js";
import { Epsilon } from "../Maths/math.constants.js";
import { Matrix, TmpVectors, Vector3 } from "../Maths/math.vector.js";
import { Plane } from "../Maths/math.plane.js";
import { Ray } from "../Culling/ray.js";
import { Vector3Distance } from "../Maths/math.vector.functions.js";
import { Clamp } from "../Maths/math.scalar.functions.js";
/**
 * @experimental
 * This class is subject to change as the geospatial camera evolves.
 *
 * Geospatial-specific camera movement system that extends the base movement with
 * raycasting and altitude-aware zoom constraints.
 *
 * This class encapsulates geospatial camera movement logic:
 * - Dragging in a way which keeps cursor anchored to globe
 * - Latitude-based pan speed dampening
 * - Zoom speed scaling based on distance to center
 * - Raycasting to determine zoom constraints based on terrain/globe
 * - Altitude-based zoom clamping
 * - Zoom direction calculation (towards cursor vs along look vector)
 */
export class GeospatialCameraMovement extends CameraMovement {
    constructor(scene, limits, cameraPosition, _cameraCenter, _cameraLookAt, pickPredicate, behavior) {
        super(scene, cameraPosition, behavior);
        this.limits = limits;
        this._cameraCenter = _cameraCenter;
        this._cameraLookAt = _cameraLookAt;
        this.zoomToCursor = true;
        this._hitPointRadius = undefined;
        this._dragPlane = new Plane(0, 0, 0, 0);
        this._dragPlaneNormal = Vector3.Zero();
        this._dragPlaneOriginPointEcef = Vector3.Zero();
        this._dragPlaneHitPointLocal = Vector3.Zero();
        this._previousDragPlaneHitPointLocal = Vector3.Zero();
        this.pickPredicate = pickPredicate;
        this._tempPickingRay = new Ray(this._cameraPosition, this._cameraLookAt);
        this.panInertia = 0;
        this.rotationInertia = 0;
        this.rotationXSpeed = Math.PI / 500; // Move 1/500th of a half circle per pixel
        this.rotationYSpeed = Math.PI / 500; // Move 1/500th of a half circle per pixel
        this.zoomSpeed = 2; // Base zoom speed; actual speed is scaled based on altitude
    }
    startDrag(pointerX, pointerY) {
        const pickResult = this._scene.pick(pointerX, pointerY, this.pickPredicate);
        if (pickResult.pickedPoint && pickResult.ray) {
            // Store radius from earth center to pickedPoint, used when calculating drag plane
            this._hitPointRadius = pickResult.pickedPoint.length();
            this._recalculateDragPlaneHitPoint(this._hitPointRadius, pickResult.ray, TmpVectors.Matrix[0]);
            this._previousDragPlaneHitPointLocal.copyFrom(this._dragPlaneHitPointLocal);
        }
        else {
            this._hitPointRadius = undefined; // can't drag without a hit on the globe
        }
    }
    stopDrag() {
        this._hitPointRadius = undefined;
    }
    /**
     * The previous drag plane hit point in local space is stored to compute the movement delta.
     * As the drag movement occurs, we will continuously recalculate this point. The delta between the previous and current hit points is the delta we will apply to the camera's localtranslation
     * @param hitPointRadius The distance between the world origin (center of globe) and the initial drag hit point
     * @param ray The ray from the camera to the new cursor location
     * @param localToEcefResult The matrix to convert from local to ECEF space
     */
    _recalculateDragPlaneHitPoint(hitPointRadius, ray, localToEcefResult) {
        // Use the camera's geocentric normal to find the dragPlaneOriginPoint which lives at hitPointRadius along the camera's geocentric normal
        this._cameraPosition.normalizeToRef(this._dragPlaneNormal);
        this._dragPlaneNormal.scaleToRef(hitPointRadius, this._dragPlaneOriginPointEcef);
        // The dragPlaneOffsetVector will later be recalculated when drag occurs, and the delta between the offset vectors will be applied to localTranslation
        ComputeLocalBasisToRefs(this._dragPlaneOriginPointEcef, TmpVectors.Vector3[0], TmpVectors.Vector3[1], TmpVectors.Vector3[2]);
        const localToEcef = Matrix.FromXYZAxesToRef(TmpVectors.Vector3[0], TmpVectors.Vector3[1], TmpVectors.Vector3[2], localToEcefResult);
        localToEcef.setTranslationFromFloats(this._dragPlaneOriginPointEcef.x, this._dragPlaneOriginPointEcef.y, this._dragPlaneOriginPointEcef.z);
        const ecefToLocal = localToEcef.invertToRef(TmpVectors.Matrix[1]);
        // Now create a plane at that point, perpendicular to the camera's geocentric normal
        Plane.FromPositionAndNormalToRef(this._dragPlaneOriginPointEcef, this._dragPlaneNormal, this._dragPlane);
        // Lastly, find the _dragPlaneHitPoint where the ray intersects the _dragPlane.
        if (IntersectRayWithPlaneToRef(ray, this._dragPlane, this._dragPlaneHitPointLocal)) {
            // If hit, convert the drag plane hit point into the local space.
            Vector3.TransformCoordinatesToRef(this._dragPlaneHitPointLocal, ecefToLocal, this._dragPlaneHitPointLocal);
        }
    }
    handleDrag(pointerX, pointerY) {
        if (this._hitPointRadius) {
            const pickResult = this._scene.pick(pointerX, pointerY);
            if (pickResult.ray) {
                const localToEcef = TmpVectors.Matrix[0];
                this._recalculateDragPlaneHitPoint(this._hitPointRadius, pickResult.ray, localToEcef);
                const delta = this._dragPlaneHitPointLocal.subtractToRef(this._previousDragPlaneHitPointLocal, TmpVectors.Vector3[6]);
                this._previousDragPlaneHitPointLocal.copyFrom(this._dragPlaneHitPointLocal);
                Vector3.TransformNormalToRef(delta, localToEcef, delta);
                this._dragPlaneOriginPointEcef.addInPlace(delta);
                this.panAccumulatedPixels.subtractInPlace(delta);
            }
        }
    }
    /** @override */
    computeCurrentFrameDeltas() {
        const cameraCenter = this._cameraCenter;
        // Slows down panning near the poles
        if (this.panAccumulatedPixels.lengthSquared() > Epsilon) {
            const centerRadius = cameraCenter.length(); // distance from planet origin to camera center
            const currentRadius = this._cameraPosition.length();
            // Dampen the pan speed based on latitude (slower near poles)
            const sineOfSphericalLat = centerRadius === 0 ? 0 : cameraCenter.z / centerRadius;
            const cosOfSphericalLat = Math.sqrt(1 - Math.min(1, sineOfSphericalLat * sineOfSphericalLat));
            const latitudeDampening = Math.sqrt(Math.abs(cosOfSphericalLat)); // sqrt here reduces effect near equator
            // Reduce the dampening effect near surface (so that at ground level, pan speed is not affected by latitude)
            const height = Math.max(currentRadius - centerRadius, Epsilon);
            const latitudeDampeningScale = Math.max(1, centerRadius / height);
            this._panSpeedMultiplier = Clamp(latitudeDampeningScale * latitudeDampening, 0, 1);
        }
        else {
            this._panSpeedMultiplier = 1;
        }
        // If a pan drag or rotate is occurring, stop zooming.
        let zoomTargetDistance;
        if (this.isDragging || this.rotationAccumulatedPixels.lengthSquared() > Epsilon) {
            this._zoomSpeedMultiplier = 0;
            this._zoomVelocity = 0;
        }
        else {
            zoomTargetDistance = this.computedPerFrameZoomPickPoint ? Vector3Distance(this._cameraPosition, this.computedPerFrameZoomPickPoint) : undefined;
            // Scales zoom movement speed based on camera distance to zoom target.
            this._zoomSpeedMultiplier = (zoomTargetDistance ?? Vector3Distance(this._cameraPosition, cameraCenter)) * 0.01;
        }
        super.computeCurrentFrameDeltas();
    }
    get isDragging() {
        return this._hitPointRadius !== undefined;
    }
    handleZoom(zoomDelta, toCursor) {
        if (zoomDelta !== 0) {
            this.zoomAccumulatedPixels += zoomDelta;
            const pickResult = this._scene.pick(this._scene.pointerX, this._scene.pointerY, this.pickPredicate);
            if (toCursor && pickResult.hit && pickResult.pickedPoint && pickResult.ray && this.zoomToCursor) {
                this.computedPerFrameZoomPickPoint = pickResult.pickedPoint;
            }
            else {
                // If no hit under cursor or explicitly told not to zoom to cursor, zoom along lookVector instead
                const lookPickResult = this.pickAlongVector(this._cameraLookAt);
                this.computedPerFrameZoomPickPoint = lookPickResult?.pickedPoint ?? undefined;
            }
        }
    }
    pickAlongVector(vector) {
        this._tempPickingRay.origin.copyFrom(this._cameraPosition);
        this._tempPickingRay.direction.copyFrom(vector);
        return this._scene.pickWithRay(this._tempPickingRay, this.pickPredicate);
    }
}
export function ClampCenterFromPolesInPlace(center) {
    const sineOfSphericalLatitudeLimit = 0.998749218; // ~90 degrees
    const centerMagnitude = center.length(); // distance from planet origin
    if (centerMagnitude > Epsilon) {
        const sineSphericalLat = centerMagnitude === 0 ? 0 : center.z / centerMagnitude;
        if (Math.abs(sineSphericalLat) > sineOfSphericalLatitudeLimit) {
            // Clamp the spherical latitude (and derive longitude)
            const sineOfClampedSphericalLat = Clamp(sineSphericalLat, -sineOfSphericalLatitudeLimit, sineOfSphericalLatitudeLimit);
            const cosineOfClampedSphericalLat = Math.sqrt(1 - sineOfClampedSphericalLat * sineOfClampedSphericalLat);
            const longitude = Math.atan2(center.y, center.x);
            // Spherical to Cartesian
            const newX = centerMagnitude * Math.cos(longitude) * cosineOfClampedSphericalLat;
            const newY = centerMagnitude * Math.sin(longitude) * cosineOfClampedSphericalLat;
            const newZ = centerMagnitude * sineOfClampedSphericalLat;
            center.set(newX, newY, newZ);
        }
    }
    return center;
}
function IntersectRayWithPlaneToRef(ray, plane, ref) {
    // Distance along the ray to the plane; null if no hit
    const dist = ray.intersectsPlane(plane);
    if (dist !== null && dist >= 0) {
        ray.origin.addToRef(ray.direction.scaleToRef(dist, TmpVectors.Vector3[0]), ref);
        return true;
    }
    return false;
}
/**
 * Helper to build east/north/up basis vectors at a world position.
 * @internal
 */
export function ComputeLocalBasisToRefs(worldPos, refEast, refNorth, refUp) {
    // up = normalized position (geocentric normal)
    refUp.copyFrom(worldPos).normalize();
    // east = normalize(worldNorth × up)
    // (cross product of Earth rotation axis with up gives east except near poles)
    const worldNorth = Vector3.LeftHandedForwardReadOnly; // (0,0,1)
    Vector3.CrossToRef(worldNorth, refUp, refEast);
    // at poles, cross with worldRight instead
    if (refEast.lengthSquared() < Epsilon) {
        Vector3.CrossToRef(Vector3.Right(), refUp, refEast);
    }
    refEast.normalize();
    // north = up × east (completes right-handed basis)
    Vector3.CrossToRef(refUp, refEast, refNorth);
    refNorth.normalize();
}
//# sourceMappingURL=geospatialCameraMovement.js.map