import { LinesMesh, Observable, Scalar, VertexBuffer, VertexData, Vector3 } from '../loader/babylonjs-import';
import { CoordinateTracker } from './CoordinateTracker';
import { copyVertex3ToRef } from '../math';
/**
 * Represents a dynamic polyline. It is possible to add, insert and remove points to it and
 * have those changes reflect its 3D visualization. Useful for tools that need to
 * calculate distances between start and end points etc.
 */
export class PolyLine {
    name;
    _api;
    static _tmp = {
        vector3A: Vector3.Zero(),
        vector3B: Vector3.Zero()
    };
    _coordinateTracker;
    _linesMesh;
    static _renderingGroupId = 2;
    /**
     * Triggered when {@link apply} is called or when camera moves around. By listening to this
     * event it is possible to know when new points are added, updated and removed. It is also possible
     * to see how far away the points are from the camera. What position they have in 2D space (perhaps one wants
     * to add a HTML element to the DOM there.).
     */
    onPointTrackableScreen = new Observable();
    /**
     * Points that make up the polyline. Use usual array operations to add, remove
     * insert points etc. When changes have been made call {@link apply} in
     * order to visualize the changes as a {@link LinesMesh}.
     */
    points = [];
    /**
     * Polyline constructor.
     * @param name Name of polyline
     * @param _api {@link BimCoreApi} instance
     * @param options - Options for line creation
     */
    constructor(name, _api, options) {
        this.name = name;
        this._api = _api;
        this._coordinateTracker = new CoordinateTracker(_api);
        this.onPointTrackableScreen = this._coordinateTracker.onUpdateObservable;
        // TODO Get parent node. (Create if not exists)
        this._linesMesh = new LinesMesh(name, _api.viewer.scene);
        this._linesMesh.renderingGroupId = PolyLine._renderingGroupId; // Render on top of everything. Otherwise it is difficult to see the line.
        this._linesMesh.setEnabled(false);
        // Create a line mesh with max number of points. We need to preallocate number of
        // points since it is not possible to change it without creating a new geometry.
        const maxLineCount = Scalar.Clamp((options?.maxPointCount ?? 50) - 1, 1, 49); // Must have at least two points in a line
        const lines = [];
        const line = [Vector3.Zero(), Vector3.Zero()];
        for (let i = 0; i < maxLineCount; ++i) {
            lines.push(line);
        }
        const vertexData = VertexData.CreateLineSystem({ lines });
        vertexData.applyToMesh(this._linesMesh, true);
    }
    /**
     * Color of the line.
     * @return The color
     */
    get color() {
        return this._linesMesh.color;
    }
    /**
     * Color of the line
     * @param c The color the line shall have.
     */
    set color(c) {
        this._linesMesh.color.copyFrom(c);
    }
    /**
     * Makes it possible to iterate each line that {@link points} represents
     * @param predicate Called for each line with start and end vector. If predicate returns `false` then iteration is stopped.
     */
    forEachLine(predicate) {
        const pLen = this.points.length;
        for (let i = 1; i < pLen; i++) {
            if (!predicate(this.points[i - 1], this.points[i])) {
                break;
            }
        }
    }
    /**
     * Gets the distance of the polyline.
     * @param o Optional. Specifies point where distance calculation should end. That is distance from first point up to this point.
     * @returns distance.
     */
    distance(o) {
        let distance = 0;
        this.forEachLine((start, end) => {
            const _continue = o === undefined || o.end !== start;
            if (_continue) {
                const s = copyVertex3ToRef(start, PolyLine._tmp.vector3A);
                const e = copyVertex3ToRef(end, PolyLine._tmp.vector3B);
                distance += Vector3.Distance(s, e);
            }
            return _continue;
        });
        return distance;
    }
    /**
     * Apply changes made to the {@link points} array. This will
     * mirror the changes in the visual representation of the polygon line.
     * It will also trigger {@link onPointTrackableScreen} events.
     */
    apply() {
        this._api.viewer.wakeRenderLoop();
        if (this.points.length < 2) {
            // Must have more than two points to create a line. If we no longer
            // have a line we disable the lines mesh and remove all no longer used points
            // from the coordinate tracker
            const existingPoints = [...this._coordinateTracker.entries()];
            for (const [id, point] of existingPoints) {
                if (point !== this.points[0]) {
                    this._coordinateTracker.untrack(id);
                }
            }
            this._linesMesh.setEnabled(false);
            return;
        }
        const positions = this._linesMesh.getVerticesData(VertexBuffer.PositionKind);
        const posLen = positions.length;
        let posIndex = 0;
        // Copy the points into the mesh geometry. Geometry
        // consists of pairs of lines defined by two vertices.
        // therefore [startX, startY, startZ, endX, endY, endZ]
        // is a line definition.
        this.forEachLine((start, end) => {
            positions[posIndex++] = start.x;
            positions[posIndex++] = start.y;
            positions[posIndex++] = start.z;
            positions[posIndex++] = end.x;
            positions[posIndex++] = end.y;
            positions[posIndex++] = end.z;
            // Ensure we do not go out of positions bounds
            // if there are more points than we anticipated
            return posIndex < posLen;
        });
        // If we have more preallocated vertices than we have points
        // then we repeat the last point until all vertices have been written.
        const lastPoint = this.points[this.points.length - 1];
        for (; posIndex < posLen; posIndex += 3) {
            positions[posIndex] = lastPoint.x;
            positions[posIndex + 1] = lastPoint.y;
            positions[posIndex + 2] = lastPoint.z;
        }
        this._linesMesh.updateVerticesData(VertexBuffer.PositionKind, positions, true, false);
        // Remove all points from coordinate tracker that are no longer in use (will trigger remove
        // status in coordinate tracker event
        const uniquePoints = new Set(this.points);
        for (const [id, p] of [...this._coordinateTracker.entries()]) {
            if (!uniquePoints.has(p)) {
                this._coordinateTracker.untrack(id);
            }
        }
        // Add all points to coordinate tracker that does not exist in coordinate tracker.
        // (Will trigger add status in coordinate tracker event)
        for (const p of [...uniquePoints]) {
            if (!this._coordinateTracker.get(p)) {
                this._coordinateTracker.track(p, p);
            }
        }
        // Force recalculation for all points (even thouse that were added above)
        // because existing points may have had their x,y,z coordinates updated.
        this._coordinateTracker.forceRecalculation();
        this._linesMesh.setEnabled(true);
    }
    /**
     * Sets point array to zero and disables visibility for the lines mesh.
     * Also removes coordinate trackers for labels if present.
     */
    clear() {
        this._coordinateTracker.clear();
        this.points.length = 0;
        this._linesMesh.setEnabled(false);
    }
    /**
     * Sets point array to zero and disables visibility for the lines mesh.
     * Also removes coordinate trackers for labels if present.
     * And disposes the linemesh.
     */
    dispose() {
        this.clear();
        this._linesMesh.dispose();
    }
}
//# sourceMappingURL=PolyLine.js.map