import { Vector3, Matrix, Camera, Viewport, Observable } from '../loader/babylonjs-import';
import { createNullFrustum, isPointInFrustum } from '../math/Frustum';
import { asWriteable } from '../Types';
import { boundingClientRectCache } from '../BoundingClientRectCache';
/**
 * Represents the state a tracked coordinate may have.
 */
export var TrackCoordinate2DState;
(function (TrackCoordinate2DState) {
    /**
     * Item is not tracked
     */
    TrackCoordinate2DState[TrackCoordinate2DState["Detached"] = 0] = "Detached";
    /**
     * Item was added. Triggers one notification
     * Next notification will say that it is {@link TrackCoordinate2DState.Updated}.
     */
    TrackCoordinate2DState[TrackCoordinate2DState["Added"] = 1] = "Added";
    /**
     * Item was updated. Will trigger notification.
     */
    TrackCoordinate2DState[TrackCoordinate2DState["Updated"] = 2] = "Updated";
    /**
     * Item was deleted. Will trigger one final notification and then no more.
     */
    TrackCoordinate2DState[TrackCoordinate2DState["Deleted"] = 3] = "Deleted";
})(TrackCoordinate2DState || (TrackCoordinate2DState = {}));
/**
 * CoordinateTracker contains methods for tracking a screen space coordinate from a 3D position.
 */
export class CoordinateTracker {
    bimApi;
    static _tmp = {
        vector3A: Vector3.Zero(),
        vector3B: Vector3.Zero(),
        identity: Matrix.Identity(),
        frustum: createNullFrustum(),
        viewPort: new Viewport(0, 0, 0, 0),
        vertex2A: { x: 0, y: 0 }
    };
    static _currentId = 1;
    _tracked = new Map();
    _callback;
    _scene;
    _cameraState;
    _materialState = {};
    _onEndFrameObserver;
    /**
     * Called when a tracked 3D coordinate changes its 2D position (when camera is manipulated).
     * Same as {@link onUpdate} but using a {@link Observable} instead.
     */
    onUpdateObservable = new Observable();
    /**
     * Constructor
     * @param bimApi BIM Core API.
     * @param recalculate2DEachFrame If `true` then it updates the 2D positions for each tracked 3D coordinate at the end of each frame.
     */
    constructor(bimApi, recalculate2DEachFrame = false) {
        this.bimApi = bimApi;
        this._scene = bimApi.viewer.scene;
        const engine = bimApi.viewer.engine;
        // At end of every frame calculate new 2D coordinates.
        this._onEndFrameObserver = engine.onEndFrameObservable.add(() => this.calculated2DCoordinates(recalculate2DEachFrame));
        this._materialState = bimApi.viewer._materials.state;
    }
    /**
     * Number of items currently being tracked.
     */
    get size() {
        return this._tracked.size;
    }
    /**
     * Allows iteration of all entities that are tracked (and their ids)
     * @returns collection of all tracked entities and their id.
     */
    *entries() {
        for (const i of this._tracked.values()) {
            yield [i.id, i.trackedCoordinate];
        }
    }
    /**
     * Attempt to get a currently tracked object by id.
     * @param id Id of tracked object.
     * @returns Tracked object if it exists. Otherwise undefined
     */
    get(id) {
        return this._tracked.get(id);
    }
    /**
     * This method takes a coordinate in 3D to track screen space position when the camera moves.
     * When method is called events on {@link onUpdateObservable} are immediately raised for the passed
     * coordinate. Same coordinate can be passed multiple times (with same id) but it will only be added once.
     * However events {@link onUpdateObservable} will still be immediately raised and report
     * 2D coordinates data even if coordinate is already tracked.
     * @param coordinate Coordinate to track.
     * @param id Optional. If not specified an id will be generated
     * @returns id of tracked coordinate.
     */
    track(coordinate, id) {
        id = id ?? CoordinateTracker._currentId++;
        let item = this._tracked.get(id);
        let wasAdded = false;
        if (item === undefined || item.trackedCoordinate !== coordinate) {
            wasAdded = true;
            item = {
                id: id,
                position: { x: 0, y: 0 },
                canvasPosition: { x: 0, y: 0 },
                visible: false,
                distance: 0,
                trackedCoordinate: coordinate,
                state: TrackCoordinate2DState.Added
            };
            this._tracked.set(id, item);
        }
        const writableItem = asWriteable(item);
        if (this._scene.activeCamera) {
            const tmp = this.projectCoordinate(this._scene.activeCamera, coordinate, id);
            const writeablePos = asWriteable(item.position);
            writeablePos.x = tmp.position.x;
            writeablePos.y = tmp.position.y;
            const writeableCanPos = asWriteable(item.canvasPosition);
            writeableCanPos.x = tmp.canvasPosition.x;
            writeableCanPos.y = tmp.canvasPosition.y;
            writableItem.visible = tmp.visible;
            writableItem.distance = tmp.distance;
        }
        // Ensure that we get immediate callback for tracked object when it is added.
        this.notifyObservers(item);
        if (wasAdded) {
            // we have notified observers of Added state so we now transition to Updated.
            writableItem.state = TrackCoordinate2DState.Updated;
        }
        return id;
    }
    /**
     * Gets 2D coordinate of a 3D coordinate once.
     * @param coordinate 3D coordinate to get 2D coordinate from
     * @param id Identifier for 3D coordinate.
     */
    trackOnce(coordinate, id) {
        if (this._scene.activeCamera) {
            return this.projectCoordinate(this._scene.activeCamera, coordinate, id);
        }
    }
    /**
     * Stops tracking of a 3D coordinate.
     * @param id Id of 3D coordinate to stop tracking
     * @returns Object that was untracked. If there is no such object then undefined.
     */
    untrack(id) {
        const ret = this._tracked.get(id);
        if (ret) {
            this._tracked.delete(id);
            ret.state = TrackCoordinate2DState.Deleted;
            this.notifyObservers(ret);
            return ret.trackedCoordinate;
        }
    }
    /**
     * Register a callback which will be called when the active camera moves. It is called once for each coordinate registered by {@link track}.
     * @param callback Callback to trigger when an active camera moves.
     */
    onUpdate(callback) {
        this._callback = callback;
    }
    /**
     * Removes all tracked coordinates from the tracker.
     */
    clear() {
        for (const t of this._tracked.values()) {
            t.state = TrackCoordinate2DState.Deleted;
            this.notifyObservers(t);
        }
        this._tracked.clear();
    }
    /**
     * Disposes the coordinate tracker.
     * */
    dispose() {
        this.clear();
        if (this._onEndFrameObserver) {
            this._scene.getEngine().onEndFrameObservable.remove(this._onEndFrameObserver);
            this._onEndFrameObserver = null;
        }
    }
    /**
     * Immediately force a recalculation of all 2D coordinates
     */
    forceRecalculation() {
        this.calculated2DCoordinates(true);
    }
    cameraOrMaterialHasChanged(camera) {
        let cameraHasChanged = false;
        if (this._cameraState === undefined || this._cameraState.camera !== camera) {
            this._cameraState = camera.twinfinity.stateSnapshot();
            cameraHasChanged = true;
        }
        else {
            cameraHasChanged = this._cameraState.isChanged;
            this._cameraState.refresh();
        }
        const materialStateChanged = this._materialState !== this.bimApi.viewer._materials.state;
        this._materialState = this.bimApi.viewer._materials.state;
        cameraHasChanged = cameraHasChanged || materialStateChanged;
        return cameraHasChanged;
    }
    /**
     * Calculate 2D coordinates for currently registered 3D coordinates.
     * @param force Calculation is normally only done if camera has changed
     * during the frame. Setting this to `true` forces the calculation
     */
    calculated2DCoordinates(force) {
        if (!this._callback && !this.onUpdateObservable.hasObservers()) {
            return;
        }
        if (this._tracked.size <= 0) {
            return;
        }
        const engine = this._scene.getEngine();
        const activeCamera = this._scene.activeCamera;
        if (!activeCamera) {
            this._cameraState = undefined;
            return;
        }
        // Switching cameras means we have a change and that we need to get a new state
        if (!force && !this.cameraOrMaterialHasChanged(activeCamera)) {
            return;
        }
        activeCamera.viewport.toGlobalToRef(engine.getRenderWidth(), engine.getRenderHeight(), CoordinateTracker._tmp.viewPort);
        const globalViewport = CoordinateTracker._tmp.viewPort;
        const frustumPlanes = activeCamera.twinfinity.getFrustumToRef(CoordinateTracker._tmp.frustum);
        const position = CoordinateTracker._tmp.vector3B;
        const rect = boundingClientRectCache.getOrAdd(this.bimApi.viewer.canvas);
        const leftOffset = rect.left + rect.document.scrollLeft;
        const topOffset = rect.top + rect.document.scrollTop;
        for (const trackedItem of this._tracked.values()) {
            if (trackedItem.trackedCoordinate.isEnabled === false) {
                continue;
            }
            const tC = trackedItem.trackedCoordinate;
            position.copyFromFloats(tC.x, tC.y, tC.z);
            // check if position is in frustum
            const inFrustum = isPointInFrustum(position, frustumPlanes);
            // project 3D position on screen
            const projectedPosition = Vector3.ProjectToRef(position, CoordinateTracker._tmp.identity, this._scene.getTransformMatrix(), globalViewport, CoordinateTracker._tmp.vector3A);
            // Notice that we reuse the trackedCoordinate instance to avoid
            // memory allocations.
            let distance = 0;
            if (activeCamera.mode === Camera.PERSPECTIVE_CAMERA) {
                distance = Vector3.Distance(activeCamera.position, position);
            }
            else {
                const orthoFrustumSize = activeCamera.twinfinity.getOrthoFrustumSizeToRef(CoordinateTracker._tmp.vertex2A);
                distance = Math.min(orthoFrustumSize.x, orthoFrustumSize.y) * 0.5;
            }
            const writeableItem = asWriteable(trackedItem);
            writeableItem.canvasPosition.x = projectedPosition.x * engine.getHardwareScalingLevel();
            writeableItem.canvasPosition.y = projectedPosition.y * engine.getHardwareScalingLevel();
            writeableItem.position.x = writeableItem.canvasPosition.x + leftOffset;
            writeableItem.position.y = writeableItem.canvasPosition.y + topOffset;
            writeableItem.visible = inFrustum;
            writeableItem.distance = distance;
            writeableItem.state = TrackCoordinate2DState.Updated;
            this.notifyObservers(trackedItem);
        }
    }
    notifyObservers(tmp) {
        if (tmp.trackedCoordinate.isEnabled === false) {
            return;
        }
        if (this._callback) {
            this._callback(tmp);
        }
        this.onUpdateObservable.notifyObservers(tmp);
    }
    projectCoordinate(camera, coordinate, id) {
        const vector = new Vector3();
        vector.set(coordinate.x, coordinate.y, coordinate.z);
        const frustumPlanes = camera.twinfinity.getFrustumToRef(CoordinateTracker._tmp.frustum);
        const engine = this._scene.getEngine();
        camera.viewport.toGlobalToRef(engine.getRenderWidth(), engine.getRenderHeight(), CoordinateTracker._tmp.viewPort);
        const globalViewport = CoordinateTracker._tmp.viewPort;
        // check if position is in frustum
        const inFrustum = isPointInFrustum(vector, frustumPlanes);
        // project 3D position on screen
        const canvasBounds = boundingClientRectCache.getOrAdd(this.bimApi.viewer.canvas);
        const documentElementBounds = boundingClientRectCache.getOrAdd(document.documentElement);
        const leftOffset = canvasBounds.left + documentElementBounds.scrollLeft;
        const topOffset = canvasBounds.top + documentElementBounds.scrollTop;
        const projectedPosition = Vector3.ProjectToRef(vector, CoordinateTracker._tmp.identity, this._scene.getTransformMatrix(), globalViewport, CoordinateTracker._tmp.vector3A);
        const canvasPosition = {
            x: projectedPosition.x * engine.getHardwareScalingLevel(),
            y: projectedPosition.y * engine.getHardwareScalingLevel()
        };
        return {
            id,
            position: {
                x: canvasPosition.x + leftOffset,
                y: canvasPosition.y + topOffset
            },
            canvasPosition,
            visible: inFrustum,
            distance: Vector3.Distance(camera.position, vector),
            trackedCoordinate: coordinate,
            state: TrackCoordinate2DState.Detached
        };
    }
}
//# sourceMappingURL=CoordinateTracker.js.map