/*!
 * Copyright (c) 2026 Vertex Software LLC. All rights reserved.
 */
import { BoundingBox, BoundingSphere, Matrix4, Quaternion, Ray, Vector3, } from "@vertexvis/geometry";
import { UUID } from "@vertexvis/utils";
import { constrainViewVector, updateLookAtRelativeToBoundingBoxCenter, } from "../rendering/vectors";
import { DEFAULT_TIMEOUT_IN_MS } from "../stream/dispatcher";
import { ClippingPlanes, FrameCamera, FrameOrthographicCamera, FramePerspectiveCamera, } from "../types";
import { withPositionAndViewVector } from "../types/frameCamera";
import { CameraRenderResult } from "./cameraRenderResult";
import { buildFlyToOperation } from "./mapper";
export class TerminalFlyToExecutor {
    constructor(flyToOptions) {
        this.flyToOptions = flyToOptions;
    }
    build() {
        return this.flyToOptions;
    }
}
export class FlyToExecutor {
    withItemId(id) {
        return new TerminalFlyToExecutor({
            flyTo: {
                type: 'internal',
                data: id,
            },
        });
    }
    withSuppliedId(id) {
        return new TerminalFlyToExecutor({
            flyTo: {
                type: 'supplied',
                data: id,
            },
        });
    }
    withCamera(camera) {
        return new TerminalFlyToExecutor({
            flyTo: {
                type: 'camera',
                data: camera,
            },
        });
    }
    withBoundingBox(boundingBox) {
        return new TerminalFlyToExecutor({
            flyTo: {
                type: 'bounding-box',
                data: boundingBox,
            },
        });
    }
    build() {
        return this.flyToOptions;
    }
}
/**
 * The `Camera` class contains properties that reflect a world space position, a
 * view direction (lookAt), and normalized vector representing the up direction.
 *
 * It also provides utility methods to update orientation of the camera and
 * rerender the scene.
 *
 * This class in intended to be treated as an immutable type. Any mutations return
 * a new instance of the class with the updated properties.
 */
export class Camera {
    constructor(stream, aspect, data, boundingBox, decodeFrame, flyToOptions) {
        this.stream = stream;
        this.aspect = aspect;
        this.data = data;
        this.boundingBox = boundingBox;
        this.decodeFrame = decodeFrame;
        this.flyToOptions = flyToOptions;
    }
    /**
     * Updates the position of the camera such that the given bounding box will
     * be contained within the camera's view.
     *
     * @param boundingBox The bounding box to position to.
     */
    fitToBoundingBox(boundingBox) {
        return this.flyTo({
            boundingBox,
        });
    }
    /**
     * Returns the distance from the camera's position to the center
     * of the provided bounding box (or the scene's visible bounding box if not provided).
     *
     * @param boundingBox - The bounding box to determine distance from.
     */
    signedDistanceToBoundingBoxCenter(boundingBox) {
        const { position, viewVector } = withPositionAndViewVector(this.data);
        const boundingBoxCenter = BoundingBox.center(boundingBox !== null && boundingBox !== void 0 ? boundingBox : this.boundingBox);
        const cameraToCenter = Vector3.subtract(boundingBoxCenter, position);
        return (Vector3.dot(viewVector, cameraToCenter) / Vector3.magnitude(viewVector));
    }
    /**
     * Specifies that the next render of the camera will be repositioned to one of
     * the options specified in `options`.
     *
     * @example
     * ```typescript
     * const viewer = document.querySelector("vertex-viewer");
     * const scene = await viewer.scene();
     * const camera = scene.camera();
     *
     * // Fly to and fit to a specific item by ID with an animation of 1 second
     * await camera
     *   .flyTo({ itemId: "item-id" })
     *   .render({ animation: { milliseconds: 1000 } });
     *
     * // Fly to and fit to a specific item by supplied ID with an animation of 1 second
     * await camera
     *   .flyTo({ itemSuppliedId: "item-supplied-id" })
     *   .render({ animation: { milliseconds: 1000 } });
     *
     * // Fly to and fit to the bounding box of the current set of selected items
     * // with an animation of 1 second
     * await camera
     *   .flyTo({
     *     boundingBox:
     *       viewer.frame.scene.sceneViewSummary.selectedVisibleSummary.boundingBox,
     *   })
     *   .render({ animation: { milliseconds: 1000 } });
     *
     * // Fly to a specific camera
     * await camera
     *   .flyTo({
     *     camera: {
     *       position: {
     *         x: 1,
     *         y: 2,
     *         z: 3,
     *       },
     *       lookAt: {
     *         x: 0,
     *         y: 0,
     *         z: 0,
     *       },
     *       up: {
     *         x: 0,
     *         y: 1,
     *         z: 0,
     *       },
     *     },
     *   })
     *   .render({ animation: { milliseconds: 1000 } });
     *
     * // Fly to scene items specified by a query
     * await camera
     *   .flyTo({
     *     sceneItemQueryExpression: {
     *       operand: {
     *         metadata: {
     *           valueFilter: 'WheelHub',
     *           keys: ['ProductName'],
     *         },
     *       },
     *     },
     *   })
     * ```
     *
     * @param paramsOrQuery An object or query describing how the camera should
     * be positioned.
     */
    flyTo(paramsOrQuery) {
        if (typeof paramsOrQuery !== 'function') {
            return this.updateFlyToOptions({
                flyTo: this.buildFlyToType(paramsOrQuery),
            });
        }
        else {
            return this.updateFlyToOptions(paramsOrQuery(new FlyToExecutor()).build());
        }
    }
    /**
     * Performs a `flyTo` operation with the current visible bounding box of
     * the scene.
     *
     * @example
     * ```typescript
     * const viewer = document.querySelector('vertex-viewer');
     * const scene = await viewer.scene();
     * const camera = scene.camera();
     *
     * // Fit to the visible bounding box of the scene with an animation of 1 second
     * await camera.viewAll().render({ animation: { milliseconds: 1000 } });
     * ```
     */
    viewAll() {
        return this.fitToBoundingBox(this.boundingBox);
    }
    /**
     * Queues the rendering for a new frame using this camera. The returned
     * promise will resolve when a frame is received that contains this camera.
     */
    async render(renderOptions) {
        var _a, _b, _c;
        if (this.flyToOptions == null && renderOptions != null) {
            this.flyToOptions = {
                flyTo: {
                    type: 'camera',
                    data: this.data,
                },
            };
        }
        try {
            const corrId = UUID.create();
            if (this.flyToOptions != null) {
                const payload = buildFlyToOperation(corrId, this.flyToOptions, renderOptions === null || renderOptions === void 0 ? void 0 : renderOptions.animation, this.toFrameCamera());
                const flyToResponse = await this.stream.flyTo(payload, true);
                return new CameraRenderResult(this.stream, this.decodeFrame, {
                    correlationId: corrId,
                    animationId: ((_b = (_a = flyToResponse.flyTo) === null || _a === void 0 ? void 0 : _a.animationId) === null || _b === void 0 ? void 0 : _b.hex) || undefined,
                }, ((_c = renderOptions === null || renderOptions === void 0 ? void 0 : renderOptions.animation) === null || _c === void 0 ? void 0 : _c.milliseconds) != null
                    ? renderOptions.animation.milliseconds + DEFAULT_TIMEOUT_IN_MS
                    : undefined);
            }
            else {
                this.stream.replaceCamera({
                    camera: FrameCamera.toProtobuf(this.data),
                    frameCorrelationId: { value: corrId },
                });
                return new CameraRenderResult(this.stream, this.decodeFrame, {
                    correlationId: corrId,
                });
            }
        }
        catch (e) {
            console.warn('Error when performing render: ', e);
            throw e;
        }
    }
    /**
     * Repositions the camera by rotating its current position around an axis placed
     * at the `lookAt` point. This method internally will call {@link Camera.rotateAroundAxisAtPoint}
     * with the `point` parameter set to the current `lookAt` point.
     *
     * @see {@link Camera.rotateAroundAxisAtPoint} for more information.
     *
     * @param angleInRadians The angle, in radians, to rotate.
     * @param axis A normalized vector to rotate around.
     */
    rotateAroundAxis(angleInRadians, axis) {
        return this.rotateAroundAxisAtPoint(angleInRadians, this.data.lookAt, axis);
    }
    /**
     * Aligns the camera to the plane defined by the provided position and normal.
     * This will place the camera at the provided position, set the up vector to
     * the provided normal, and place the lookAt on the defined plane. The point
     * chosen for the lookAt will be determined using the current view vector.
     *
     * @example
     * ```typescript
     * const viewer = document.querySelector("vertex-viewer");
     *
     * viewer.addEventListener("tap", async (event) => {
     *   const scene = await viewer.scene();
     *   const raycaster = scene.raycaster();
     *
     *   const [hit] = await raycaster.hitItems(event.detail.position);
     *
     *   if (hit != null) {
     *     const camera = scene.camera();
     *
     *     // Align to the plane represented by the hit result with an animation of 1 second
     *     await camera
     *       .alignTo(hit.hitPoint, hit.hitNormal)
     *       .render({ animation: { milliseconds: 1000 } });
     *   }
     * });
     * ```
     *
     * @param position The position to place the camera at.
     * @param normal The normal of the plane to align to.
     */
    alignTo(position, normal) {
        const worldX = Vector3.normalize(Vector3.cross(this.up, Vector3.normalize(this.viewVector)));
        const positiveWorldY = Vector3.normalize(Vector3.cross(Vector3.normalize(this.viewVector), worldX));
        // Invert the world y axis if the provided normal is more than 90 degrees from it
        // to compute a proper angle to rotate the view vector by
        const worldY = Vector3.angleTo(normal, positiveWorldY) > Math.PI / 2
            ? Vector3.negate(positiveWorldY)
            : positiveWorldY;
        const localX = Vector3.isEqual(worldY, normal)
            ? worldX
            : Vector3.normalize(Vector3.cross(worldY, normal));
        const transformedViewVector = Vector3.transformMatrix(this.viewVector, Matrix4.makeRotation(Quaternion.fromAxisAngle(localX, Vector3.angleTo(normal, worldY))));
        const lookAtRay = Ray.create({
            origin: position,
            direction: Vector3.normalize(transformedViewVector),
        });
        const lookAt = Ray.at(lookAtRay, Vector3.magnitude(this.viewVector));
        return this.update({
            position,
            lookAt,
            up: normal,
        });
    }
    /**
     * Updates the `position` and `up` vectors of the camera to the given standard
     * view, and sets the `lookAt` point to the origin.
     *
     * @see {@link StandardView} for the available standard views.
     *
     * @param standardView The standard view to apply.
     * @returns A new camera.
     */
    standardView(standardView) {
        return this.update({
            position: standardView.position,
            viewVector: Vector3.subtract(Vector3.origin(), standardView.position),
            lookAt: Vector3.origin(),
            up: standardView.up,
        });
    }
    /**
     * Updates the `position` and `up` vectors of the camera to the given standard
     * view, maintaining the existing `lookAt` point.
     *
     * @see {@link StandardView} for the available standard views.
     *
     * @param standardView The standard view to apply.
     * @returns A new camera.
     */
    standardViewFixedLookAt(standardView) {
        const standardViewRay = Ray.create({
            origin: this.lookAt,
            direction: Vector3.normalize(standardView.position),
        });
        const updatedPosition = Ray.at(standardViewRay, Vector3.magnitude(this.viewVector));
        return this.update({
            up: standardView.up,
            position: updatedPosition,
            viewVector: Vector3.subtract(this.lookAt, updatedPosition),
        });
    }
    buildFlyToType(options) {
        if (options.boundingBox != null) {
            return { type: 'bounding-box', data: options.boundingBox };
        }
        else if (options.camera != null) {
            return { type: 'camera', data: options.camera };
        }
        else if (options.itemId != null) {
            return { type: 'internal', data: options.itemId };
        }
        else if (options.itemSuppliedId != null) {
            return { type: 'supplied', data: options.itemSuppliedId };
        }
        else if (options.sceneItemQueryExpression != null) {
            return {
                type: 'scene-item-query',
                data: options.sceneItemQueryExpression,
            };
        }
        else {
            throw new Error('Fly to must specify at least one option.');
        }
    }
    computeClippingPlanes(camera) {
        return ClippingPlanes.fromBoundingBoxAndLookAtCamera(this.boundingBox, camera);
    }
}
export class PerspectiveCamera extends Camera {
    constructor(stream, aspect, perspectiveData, boundingBox, decodeFrame, flyToOptions) {
        super(stream, aspect, perspectiveData, boundingBox, decodeFrame, flyToOptions);
        this.perspectiveData = perspectiveData;
    }
    /**
     * Shifts the position of the camera by the given delta.
     *
     * @param delta The number of units to shift the camera on the X, Y, and Z
     * axis.
     */
    moveBy(delta) {
        return this.update({
            position: Vector3.add(this.position, delta),
            lookAt: Vector3.add(this.lookAt, delta),
        });
    }
    rotateAroundAxisAtPoint(angleInRadians, point, axis) {
        return this.update({
            position: Vector3.rotateAboutAxis(angleInRadians, this.position, axis, point),
            lookAt: Vector3.rotateAboutAxis(angleInRadians, this.lookAt, axis, point),
            up: Vector3.rotateAboutAxis(angleInRadians, this.up, axis, Vector3.origin()),
        });
    }
    update(camera) {
        var _a;
        const fovY = (_a = camera.fovY) !== null && _a !== void 0 ? _a : this.fovY;
        return new PerspectiveCamera(this.stream, this.aspect, { ...this.perspectiveData, ...camera, fovY }, this.boundingBox, this.decodeFrame, this.flyToOptions);
    }
    toFrameCamera() {
        var _a;
        return new FramePerspectiveCamera(this.position, this.lookAt, this.up, this.near, this.far, this.aspectRatio, (_a = this.fovY) !== null && _a !== void 0 ? _a : 45);
    }
    get viewVector() {
        return Vector3.subtract(this.lookAt, this.position);
    }
    /**
     * The position vector for the camera, in world space coordinates.
     */
    get position() {
        return { ...this.perspectiveData.position };
    }
    /**
     * A normalized vector representing the up direction.
     */
    get up() {
        return { ...this.perspectiveData.up };
    }
    /**
     * A vector, in world space coordinates, of where the camera is pointed at.
     */
    get lookAt() {
        return { ...this.perspectiveData.lookAt };
    }
    /**
     * The camera's field of view.
     */
    get fovY() {
        return this.perspectiveData.fovY;
    }
    /**
     * The aspect ratio of the camera.
     */
    get aspectRatio() {
        return this.aspect;
    }
    /**
     * The camera's near clipping plane.
     */
    get near() {
        const { near } = this.computeClippingPlanes(this.perspectiveData);
        return near;
    }
    /**
     * The camera's far clipping plane.
     */
    get far() {
        const { far } = this.computeClippingPlanes(this.perspectiveData);
        return far;
    }
    updateFlyToOptions(flyToOptions) {
        return new PerspectiveCamera(this.stream, this.aspect, this.perspectiveData, this.boundingBox, this.decodeFrame, flyToOptions);
    }
}
export class OrthographicCamera extends Camera {
    constructor(stream, aspect, orthographicData, boundingBox, decodeFrame, flyToOptions) {
        super(stream, aspect, orthographicData, boundingBox, decodeFrame, flyToOptions);
        this.orthographicData = orthographicData;
    }
    /**
     * Shifts the position of the camera by the given delta.
     *
     * @param delta The number of units to shift the camera on the X, Y, and Z
     * axis.
     */
    moveBy(delta) {
        // The rotation point should match the lookAt point when panning
        const updatedPoint = Vector3.add(this.lookAt, delta);
        return this.update({
            lookAt: updatedPoint,
            rotationPoint: updatedPoint,
        });
    }
    rotateAroundAxisAtPoint(angleInRadians, point, axis) {
        const updatedLookAt = Vector3.rotateAboutAxis(angleInRadians, this.lookAt, axis, point);
        const updatedPosition = Vector3.rotateAboutAxis(angleInRadians, this.position, axis, point);
        const viewVector = constrainViewVector(Vector3.subtract(updatedLookAt, updatedPosition), BoundingSphere.create(this.boundingBox));
        return this.update({
            viewVector: viewVector,
            lookAt: updatedLookAt,
            up: Vector3.rotateAboutAxis(angleInRadians, this.up, axis, Vector3.origin()),
        });
    }
    update(camera) {
        return new OrthographicCamera(this.stream, this.aspect, { ...this.orthographicData, ...camera }, this.boundingBox, this.decodeFrame, this.flyToOptions);
    }
    toFrameCamera() {
        const boundingSphere = BoundingSphere.create(this.boundingBox);
        const viewVector = constrainViewVector(this.viewVector, boundingSphere);
        const lookAt = updateLookAtRelativeToBoundingBoxCenter(this.lookAt, viewVector, boundingSphere.center);
        return new FrameOrthographicCamera(viewVector, lookAt, this.up, this.near, this.far, this.aspectRatio, this.fovHeight, this.rotationPoint);
    }
    get viewVector() {
        return { ...this.orthographicData.viewVector };
    }
    get position() {
        return Vector3.add(this.lookAt, Vector3.negate(this.viewVector));
    }
    /**
     * A vector, in world space coordinates, of where the camera should be rotated around.
     * Most interactions (like pan and zoom) will update lookAt and rotationPoint to be the same point.
     * However, rotation interactions will only update lookAt and not rotationPoint.
     */
    get rotationPoint() {
        const rotationPoint = this.orthographicData.rotationPoint;
        if (rotationPoint != null) {
            return rotationPoint;
        }
        else {
            return undefined;
        }
    }
    /**
     * A normalized vector representing the up direction.
     */
    get up() {
        return { ...this.orthographicData.up };
    }
    /**
     * A vector, in world space coordinates, of where the camera is pointed at.
     */
    get lookAt() {
        return { ...this.orthographicData.lookAt };
    }
    /**
     * The camera's field of view.
     */
    get fovHeight() {
        return this.orthographicData.fovHeight;
    }
    /**
     * The aspect ratio of the camera.
     */
    get aspectRatio() {
        return this.aspect;
    }
    /**
     * The camera's near clipping plane.
     */
    get near() {
        const { near } = this.computeClippingPlanes(this.orthographicData);
        return near;
    }
    /**
     * The camera's far clipping plane.
     */
    get far() {
        const { far } = this.computeClippingPlanes(this.orthographicData);
        return far;
    }
    updateFlyToOptions(flyToOptions) {
        return new OrthographicCamera(this.stream, this.aspect, this.orthographicData, this.boundingBox, this.decodeFrame, flyToOptions);
    }
}
//# sourceMappingURL=camera.js.map
