/*!
 * Copyright (c) 2026 Vertex Software LLC. All rights reserved.
 */
import { BoundingBox, Plane, Point, Ray, Vector3 } from "@vertexvis/geometry";
import { getMouseClientPosition } from "../dom";
import { ElementRectObserver } from "../elementRectObserver";
export class TeleportInteractionHandler {
    constructor(model, animationConfiguration) {
        this.model = model;
        this.animationConfiguration = animationConfiguration;
        this.rectObserver = new ElementRectObserver();
        this.handlePointerDown = this.handlePointerDown.bind(this);
        this.handlePointerUp = this.handlePointerUp.bind(this);
        this.handleEnabledChange = this.handleEnabledChange.bind(this);
        this.handleTeleportModeChange = this.handleTeleportModeChange.bind(this);
        this.enabledChangeDisposable = this.model.onEnabledChange(this.handleEnabledChange);
    }
    dispose() {
        var _a;
        this.disable();
        (_a = this.enabledChangeDisposable) === null || _a === void 0 ? void 0 : _a.dispose();
        this.element = undefined;
        this.api = undefined;
        this.rectObserver.disconnect();
    }
    initialize(element, api) {
        this.element = element;
        this.api = api;
        this.rectObserver.observe(element);
        this.handleEnabledChange(this.model.getEnabled());
    }
    setAnimations(config) {
        this.animationConfiguration = config;
    }
    enable() {
        var _a;
        this.disable();
        this.teleportModeChangeDisposable = this.model.onTeleportModeChange(this.handleTeleportModeChange);
        (_a = this.element) === null || _a === void 0 ? void 0 : _a.addEventListener('pointerdown', this.handlePointerDown);
        this.handleTeleportModeChange(this.model.getTeleportMode());
    }
    disable() {
        var _a, _b, _c;
        (_a = this.teleportModeChangeDisposable) === null || _a === void 0 ? void 0 : _a.dispose();
        (_b = this.cursorDisposable) === null || _b === void 0 ? void 0 : _b.dispose();
        (_c = this.element) === null || _c === void 0 ? void 0 : _c.removeEventListener('pointerdown', this.handlePointerDown);
    }
    async handlePointerDown(event) {
        this.downPosition = getMouseClientPosition(event, this.rectObserver.rect);
        this.downButtons = event.buttons;
        window.addEventListener('pointerup', this.handlePointerUp);
    }
    async handlePointerUp(event) {
        var _a, _b;
        const mode = this.model.getTeleportMode();
        const threshold = (_b = (_a = this.api) === null || _a === void 0 ? void 0 : _a.pixelThreshold()) !== null && _b !== void 0 ? _b : 2;
        const point = getMouseClientPosition(event, this.rectObserver.rect);
        const isRightClick = this.downButtons === 2;
        const hasModifier = event.shiftKey || event.altKey || event.metaKey || event.ctrlKey;
        if (mode != null &&
            this.downPosition != null &&
            Point.distance(this.downPosition, point) <= threshold &&
            !isRightClick &&
            !hasModifier) {
            switch (mode) {
                case 'teleport':
                    return this.teleportToHit(point);
                case 'teleport-toward':
                    return this.teleportTowardHit(point);
                case 'teleport-and-align':
                    return this.teleportAndAlign(point);
            }
        }
        this.downPosition = undefined;
        window.removeEventListener('pointerup', this.handlePointerUp);
    }
    async beginInteraction() {
        var _a, _b;
        if (!((_a = this.api) === null || _a === void 0 ? void 0 : _a.isInteracting())) {
            await ((_b = this.api) === null || _b === void 0 ? void 0 : _b.beginInteraction());
        }
    }
    async endInteraction() {
        var _a;
        await ((_a = this.api) === null || _a === void 0 ? void 0 : _a.endInteraction());
    }
    async teleportTowardHit(point) {
        var _a, _b;
        const worldPoint = await ((_a = this.api) === null || _a === void 0 ? void 0 : _a.getWorldPointFromViewport(point));
        const mouseRay = (_b = this.api) === null || _b === void 0 ? void 0 : _b.getRayFromPoint(point);
        if (mouseRay != null) {
            const { teleportDistancePercentage, teleportCollisionDistance } = this.model.getConfiguration();
            const teleportDistanceScalar = teleportDistancePercentage / 100;
            return this.performInteraction(({ camera, boundingBox }) => {
                const stepDistance = Math.max(...Vector3.toArray(BoundingBox.lengths(boundingBox))) *
                    teleportDistanceScalar;
                const positionPlane = Plane.fromNormalAndCoplanarPoint(camera.up, camera.position);
                const projectedWorldPoint = Plane.projectPoint(positionPlane, worldPoint !== null && worldPoint !== void 0 ? worldPoint : camera.position);
                const distanceToWorldPoint = Vector3.distance(camera.position, projectedWorldPoint);
                if (distanceToWorldPoint < stepDistance &&
                    distanceToWorldPoint > teleportCollisionDistance * 1.1) {
                    const mouseRayAtWorldPoint = Ray.create({
                        ...mouseRay,
                        origin: projectedWorldPoint,
                    });
                    const nextPosition = Plane.projectPoint(positionPlane, Ray.at(mouseRayAtWorldPoint, -teleportCollisionDistance));
                    return camera.update({
                        position: nextPosition,
                        lookAt: Vector3.add(camera.lookAt, Vector3.subtract(nextPosition, camera.position)),
                    });
                }
                else {
                    const projectedNextStep = Plane.projectPoint(positionPlane, Ray.at(mouseRay, stepDistance));
                    return camera.update({
                        position: projectedNextStep,
                        lookAt: Vector3.add(camera.lookAt, Vector3.subtract(projectedNextStep, camera.position)),
                    });
                }
            });
        }
    }
    async teleportToHit(point) {
        const worldPoint = await this.getWorldPointWithFallback(point);
        if (worldPoint != null) {
            return this.performInteraction(({ camera, boundingBox }) => {
                const shortestBoundingBoxLength = this.shortestLength(boundingBox);
                const heightScalar = this.model.getTeleportHeightPercentage() / 100;
                const cameraPlane = Plane.fromNormalAndCoplanarPoint(camera.up, camera.position);
                const projectedHitPosition = Plane.projectPoint(cameraPlane, worldPoint);
                const rayToHitPosition = Ray.create({
                    origin: camera.position,
                    direction: Vector3.normalize(Vector3.subtract(projectedHitPosition, camera.position)),
                });
                const distanceToHitPosition = Vector3.distance(camera.position, projectedHitPosition);
                const distanceToLookAt = Vector3.distance(camera.position, camera.lookAt);
                const newPosition = Ray.at(rayToHitPosition, distanceToHitPosition - shortestBoundingBoxLength * heightScalar);
                const newPositionViewVectorRay = Ray.create({
                    origin: newPosition,
                    direction: Vector3.normalize(camera.viewVector),
                });
                return camera.update({
                    position: newPosition,
                    lookAt: Ray.at(newPositionViewVectorRay, distanceToLookAt),
                });
            });
        }
    }
    async teleportAndAlign(point) {
        var _a;
        const hits = await ((_a = this.api) === null || _a === void 0 ? void 0 : _a.hitItems(point));
        const hit = hits != null ? hits[0] : undefined;
        if ((hit === null || hit === void 0 ? void 0 : hit.hitNormal) != null && (hit === null || hit === void 0 ? void 0 : hit.hitPoint)) {
            await this.performInteraction(({ camera, boundingBox }) => {
                const shortestBoundingBoxLength = this.shortestLength(boundingBox);
                const heightScalar = this.model.getTeleportHeightPercentage() / 100;
                const upRay = Ray.create({
                    origin: hit.hitPoint,
                    direction: hit.hitNormal,
                });
                return camera.alignTo(Ray.at(upRay, shortestBoundingBoxLength * heightScalar), hit.hitNormal);
            });
        }
    }
    async performInteraction(fn) {
        var _a;
        await this.beginInteraction();
        await ((_a = this.api) === null || _a === void 0 ? void 0 : _a.transformCamera(fn, this.renderConfiguration()));
        await this.endInteraction();
    }
    async getWorldPointWithFallback(point) {
        var _a, _b;
        const worldPoint = await ((_a = this.api) === null || _a === void 0 ? void 0 : _a.getWorldPointFromViewport(point));
        if (worldPoint == null) {
            const hits = await ((_b = this.api) === null || _b === void 0 ? void 0 : _b.hitItems(point));
            const hit = hits != null ? hits[0] : undefined;
            return (hit === null || hit === void 0 ? void 0 : hit.hitPoint) != null
                ? hit.hitPoint
                : worldPoint;
        }
        return worldPoint;
    }
    handleEnabledChange(enabled) {
        if (enabled) {
            this.enable();
        }
        else {
            this.disable();
        }
    }
    handleTeleportModeChange(mode) {
        var _a, _b;
        (_a = this.cursorDisposable) === null || _a === void 0 ? void 0 : _a.dispose();
        if (mode != null) {
            this.cursorDisposable = (_b = this.api) === null || _b === void 0 ? void 0 : _b.addCursor('crosshair');
        }
    }
    shortestLength(boundingBox) {
        return Math.min(...Vector3.toArray(BoundingBox.lengths(boundingBox)));
    }
    renderConfiguration() {
        return this.animationConfiguration != null
            ? { animation: { milliseconds: this.animationConfiguration.durationMs } }
            : undefined;
    }
}
//# sourceMappingURL=interactions.js.map
