/*!
 * Copyright (c) 2026 Vertex Software LLC. All rights reserved.
 */
import { Vector3 } from "@vertexvis/geometry";
import { EventDispatcher } from "@vertexvis/utils";
import { Mapper } from "@vertexvis/utils";
import { fromPbVector3f } from "../../lib/mappers";
/**
 * A controller to handle point-to-point measurement interactions.
 */
export class PointToPointInteractionController {
    constructor(model) {
        this.model = model;
    }
    /**
     * Returns an interaction handler to perform a new measurement.
     *
     * @param pt A point in viewport coordinates.
     * @param hits A provider to perform hits.
     * @returns An interaction handler, or `undefined` if the point does not hit
     *  any geometry.
     */
    newMeasurement(pt, hits) {
        const world = hits.hitTester().transformPointToWorld(pt);
        return world != null
            ? new NewMeasurementInteraction(pt, world, this.model)
            : undefined;
    }
    /**
     * Returns an interaction handler to edit an existing measurement.
     *
     * @param anchor The anchor to edit.
     * @returns An interaction handler to edit a measurement.
     */
    editMeasurement(anchor) {
        const measurement = this.model.getMeasurement();
        if (measurement == null) {
            throw new Error('Cannot edit measurement. Measurement is empty.');
        }
        return anchor === 'start'
            ? new EditStartAnchorInteraction(measurement, this.model)
            : new EditEndAnchorInteraction(measurement, this.model);
    }
    /**
     * Clears the position of the feedback indicator.
     */
    clearIndicator() {
        this.model.setIndicator(undefined);
    }
    /**
     * Updates the position of the feedback indicator.
     *
     * @param pt A point, in viewport coordinates.
     * @param hits A provider to perform hits.
     * @returns `true` if the indicator is over geometry.
     */
    moveIndicator(pt, hits) {
        const worldPt = hits.hitTester().transformPointToWorld(pt);
        this.model.setIndicator(worldPt);
        return worldPt != null;
    }
}
/**
 * The model for point-to-point interactions.
 */
export class PointToPointInteractionModel {
    constructor() {
        this.measurementChanged = new EventDispatcher();
        this.indicatorChanged = new EventDispatcher();
    }
    /**
     * Returns a model that doesn't have the indicator or measurement set.
     */
    static empty() {
        return new PointToPointInteractionModel();
    }
    /**
     * Returns the current measurement for the interaction.
     */
    getMeasurement() {
        return this.measurement;
    }
    /**
     * Updates the measurement and emits a change event.
     *
     * @param measurement The new measurement.
     */
    setMeasurement(measurement) {
        if (this.measurement !== measurement) {
            this.measurement = measurement;
            this.measurementChanged.emit(measurement);
        }
    }
    /**
     * Updates the measurement from the given values. If `start` or `end` are
     * `undefined` the measurement will be cleared.
     *
     * @param start The start point.
     * @param end The end point.
     * @param valid `true` if the measurement is valid.
     */
    setMeasurementFromValues(start, end, valid) {
        if (start != null && end != null) {
            const measurement = valid
                ? validMeasurement(start, end)
                : invalidMeasurement(start, end);
            this.setMeasurement(measurement);
        }
        else {
            this.setMeasurement(undefined);
        }
    }
    /**
     * Adds a callback that is invoked when the measurement changes.
     *
     * @param listener The callback to register.
     * @returns A `Disposable` to remove the event listener.
     */
    onMeasurementChanged(listener) {
        return this.measurementChanged.on(listener);
    }
    /**
     * Gets the position representing the feedback indicator.
     */
    getIndicator() {
        return this.indicator;
    }
    /**
     * Updates the position of the feedback indicator.
     */
    setIndicator(pt) {
        if (this.indicator !== pt) {
            this.indicator = pt;
            this.indicatorChanged.emit(pt);
        }
    }
    /**
     * Adds a callback that is invoked when the feedback indicator position
     * changes.
     *
     * @param listener The callback to register.
     * @returns A `Disposable` to remove the event listener.
     */
    onIndicatorChanged(listener) {
        return this.indicatorChanged.on(listener);
    }
}
/**
 * An interaction handler to perform a new measurement. As a user is performing
 * an interaction, the handler will fetch the position of the starting anchor.
 * While fetching the position, a position from the depth buffer will be used.
 */
class NewMeasurementInteraction {
    constructor(pt, world, model) {
        this.pt = pt;
        this.world = world;
        this.model = model;
        this.hitWorldValid = true;
        // Prevents any updates of the interaction while finish is completing.
        this.preventUpdate = false;
    }
    fetchStartIfMissing(hits) {
        if (this.pendingStart == null) {
            this.pendingStart = this.fetchStart(hits);
        }
    }
    async fetchStart(hits) {
        const raycaster = await hits.raycaster();
        const hitPt = await getHit(raycaster, this.pt);
        if (hitPt == null) {
            // If the hit is empty, fallback to using a point derived from the depth
            // buffer.
            const invalidPt = hits.hitTester().transformPointToWorld(this.pt);
            this.hitWorld = invalidPt;
            this.hitWorldValid = false;
        }
        else {
            this.hitWorld = hitPt;
            this.hitWorldValid = true;
        }
    }
    update(pt, hits) {
        var _a;
        if (!this.preventUpdate) {
            this.fetchStartIfMissing(hits);
            const end = hits
                .hitTester()
                .transformPointToWorld(pt, { ignoreHitTest: true });
            const isHit = hits.hitTester().hitTest(pt);
            const start = (_a = this.hitWorld) !== null && _a !== void 0 ? _a : this.world;
            if (end == null) {
                throw new Error('Cannot update new measurement interaction. End point is empty.');
            }
            else if (!this.hitWorldValid || !isHit) {
                this.model.setIndicator(end);
                this.setMeasurement(invalidMeasurement(start, end));
            }
            else {
                this.model.setIndicator(end);
                this.setMeasurement(validMeasurement(start, end));
            }
        }
    }
    async finish(pt, hits) {
        var _a;
        this.preventUpdate = true;
        // Wait for the hit for the starting point.
        this.fetchStartIfMissing(hits);
        await this.pendingStart;
        this.model.setIndicator(undefined);
        const raycaster = await hits.raycaster();
        const hitPt = await getHit(raycaster, pt);
        const start = (_a = this.hitWorld) !== null && _a !== void 0 ? _a : this.world;
        this.preventUpdate = false;
        if (hitPt == null) {
            const end = hits
                .hitTester()
                .transformPointToWorld(pt, { ignoreHitTest: true });
            if (end == null) {
                throw new Error('Cannot complete new measurement interaction. End point is empty.');
            }
            return this.setMeasurement(invalidMeasurement(start, end));
        }
        else if (!this.hitWorldValid) {
            return this.setMeasurement(invalidMeasurement(start, hitPt));
        }
        else {
            return this.setMeasurement(validMeasurement(start, hitPt));
        }
    }
    setMeasurement(measurement) {
        this.model.setMeasurement(measurement);
        return measurement;
    }
}
/**
 * Base class for an edit anchor interaction.
 *
 * @see {@link EditStartAnchorInteraction}
 * @see {@link EditEndAnchorInteraction}
 */
class EditAnchorInteraction {
    constructor(measurement, model) {
        this.measurement = measurement;
        this.model = model;
    }
    update(pt, context) {
        const world = context
            .hitTester()
            .transformPointToWorld(pt, { ignoreHitTest: true });
        const isHit = context.hitTester().hitTest(pt);
        if (world == null) {
            throw new Error('Cannot update new measurement interaction. End point is empty.');
        }
        else if (!isHit) {
            this.model.setIndicator(world);
            this.setMeasurement(this.getInvalidMeasurement(world));
        }
        else {
            this.model.setIndicator(world);
            this.setMeasurement(this.getValidMeasurement(world));
        }
    }
    async finish(pt, hits) {
        const raycaster = await hits.raycaster();
        const hitPt = await getHit(raycaster, pt);
        this.model.setIndicator(undefined);
        if (hitPt == null) {
            const end = hits
                .hitTester()
                .transformPointToWorld(pt, { ignoreHitTest: true });
            if (end == null) {
                throw new Error('Cannot complete edit measurement interaction. End point is empty.');
            }
            return this.setMeasurement(this.getInvalidMeasurement(end));
        }
        else {
            return this.setMeasurement(this.getValidMeasurement(hitPt));
        }
    }
    setMeasurement(measurement) {
        this.model.setMeasurement(measurement);
        return measurement;
    }
}
class EditStartAnchorInteraction extends EditAnchorInteraction {
    constructor(measurement, model) {
        super(measurement, model);
    }
    getInvalidMeasurement(startPt) {
        return invalidMeasurement(startPt, this.measurement.end);
    }
    getValidMeasurement(startPt) {
        return validMeasurement(startPt, this.measurement.end);
    }
}
class EditEndAnchorInteraction extends EditAnchorInteraction {
    constructor(measurement, model) {
        super(measurement, model);
    }
    getInvalidMeasurement(endPt) {
        return invalidMeasurement(this.measurement.start, endPt);
    }
    getValidMeasurement(endPt) {
        return validMeasurement(this.measurement.start, endPt);
    }
}
async function getHit(raycaster, pt) {
    var _a;
    const hitRes = await raycaster.hitItems(pt);
    const [hit] = (_a = hitRes === null || hitRes === void 0 ? void 0 : hitRes.hits) !== null && _a !== void 0 ? _a : [];
    if ((hit === null || hit === void 0 ? void 0 : hit.hitPoint) != null) {
        const hitPt = fromPbVector3f(hit.hitPoint);
        if (Mapper.isInvalid(hitPt)) {
            throw new Error(`Invalid hit response [${hitPt.errors.join(',')}]`);
        }
        return hitPt;
    }
    else
        return undefined;
}
function validMeasurement(start, end) {
    return { start, end, distance: Vector3.distance(start, end), valid: true };
}
function invalidMeasurement(start, end) {
    return { start, end, valid: false };
}
//# sourceMappingURL=interactions.js.map
