import { Vector3, Matrix, Ray } from '../loader/babylonjs-import';
import { copyVertex3ToRef, intersectsPlaneAtToRef, vertexBetweenToRef } from '../math';
import { DynamicPolygonPointValidationResult } from './DynamicPolygonPointParent';
import { BimPointBase } from './BimPointBase';
/**
 * Represents result of a {@link DynamicPolygonPoint} move operation.
 */
export var DynamicPolygonPointMoveResult;
(function (DynamicPolygonPointMoveResult) {
    /** Result is unknown */
    DynamicPolygonPointMoveResult[DynamicPolygonPointMoveResult["Unknown"] = 0] = "Unknown";
    /** Points were moved successfully */
    DynamicPolygonPointMoveResult[DynamicPolygonPointMoveResult["Success"] = 1] = "Success";
    /** Points were not moved because the polygon would have become complex */
    DynamicPolygonPointMoveResult[DynamicPolygonPointMoveResult["ComplexPolygon"] = 2] = "ComplexPolygon";
    /** Could not move point because the ray (from mouse coordinate) did not intersect the polygon plane. */
    DynamicPolygonPointMoveResult[DynamicPolygonPointMoveResult["RayIntersectionFailed"] = 3] = "RayIntersectionFailed";
})(DynamicPolygonPointMoveResult || (DynamicPolygonPointMoveResult = {}));
/**
 * Represents result of a {@link DynamicPolygonPoint} delete operation.
 */
export var DynamicPolygonPointDeleteResult;
(function (DynamicPolygonPointDeleteResult) {
    /** Result is unknown */
    DynamicPolygonPointDeleteResult[DynamicPolygonPointDeleteResult["Unknown"] = 0] = "Unknown";
    /** Points were moved successfully */
    DynamicPolygonPointDeleteResult[DynamicPolygonPointDeleteResult["Success"] = 1] = "Success";
    /** Points were not moved because the polygon would have become complex */
    DynamicPolygonPointDeleteResult[DynamicPolygonPointDeleteResult["ComplexPolygon"] = 2] = "ComplexPolygon";
    /** Result if the set of points is insufficient */
    DynamicPolygonPointDeleteResult[DynamicPolygonPointDeleteResult["NotEnoughPoints"] = 3] = "NotEnoughPoints";
    /** Result The point is virtual */
    DynamicPolygonPointDeleteResult[DynamicPolygonPointDeleteResult["Virtual"] = 4] = "Virtual";
    /** Result if the point is protected from getting deleted. */
    DynamicPolygonPointDeleteResult[DynamicPolygonPointDeleteResult["Protected"] = 5] = "Protected";
})(DynamicPolygonPointDeleteResult || (DynamicPolygonPointDeleteResult = {}));
/**
 * Represents a point in a {@link DynamicPolygon}. Inherit from this class if additional functionality
 * is required.
 */
export class DynamicPolygonPoint extends BimPointBase {
    _api;
    plane;
    static _tmp = {
        identityMatrix: Matrix.Identity(),
        rayA: Ray.Zero(),
        vector3A: Vector3.Zero(),
        vector3B: Vector3.Zero(),
        counter: 0
    };
    _id = DynamicPolygonPoint._tmp.counter++;
    _isVirtual;
    /** @hidden */
    _previous = this;
    /** @hidden */
    _next = this;
    /** @hidden */
    _parent;
    recalculateOrGetWorldCoords(force = false) {
        if (this._recalculateWorldCoords || force) {
            Vector3.TransformCoordinatesFromFloatsToRef(this._localX, this._localY, this._localZ, this._parent.worldMatrix(), DynamicPolygonPoint._tmp.vector3B);
            this._worldX = DynamicPolygonPoint._tmp.vector3B.x;
            this._worldY = DynamicPolygonPoint._tmp.vector3B.y;
            this._worldZ = DynamicPolygonPoint._tmp.vector3B.z;
            this._recalculateWorldCoords = false;
        }
        return { x: this._worldX, y: this._worldY, z: this._worldZ };
    }
    recalculateOrGetLocalCoords(force = false) {
        if (this._recalculateLocalCoords || force) {
            Vector3.TransformCoordinatesFromFloatsToRef(this._worldX, this._worldY, this._worldZ, this._parent.worldMatrix().clone().invert(), DynamicPolygonPoint._tmp.vector3B);
            this._localX = DynamicPolygonPoint._tmp.vector3B.x;
            this._localY = DynamicPolygonPoint._tmp.vector3B.y;
            this._localZ = DynamicPolygonPoint._tmp.vector3B.z;
            this._recalculateLocalCoords = false;
        }
        return { x: this._localX, y: this._localY, z: this._localZ };
    }
    /** When `true` then {@link DynamicPolygon.onPointTrackableScreen} will trigger for this instance. Otherwise it will not */
    isEnabled = true;
    /**
     *
     * @param _api {@link BimCoreApi} API.
     * @param plane [Plane](https://doc.babylonjs.com/typedoc/classes/babylon.plane) that this point lies on.
     * @param isVirtual whether point is virtual or not.
     */
    constructor(_api, plane, isVirtual) {
        super();
        this._api = _api;
        this.plane = plane;
        this._isVirtual = isVirtual;
        this._previous = this;
        this._next = this;
    }
    /**
     * Predicate to check if a point is virtual or not.
     * @param p Point to check
     * @returns `true` if point is not virtual, otherwise `false`.
     */
    static IsNotVirtual(p) {
        return !p.virtual();
    }
    /**
     * Same as calling {@link DynamicPolygon.apply}.
     */
    apply() {
        this._parent.apply();
    }
    /**
     * Check if `this` point can be deleted or not.
     * @returns a {@link DynamicPolygonPointDeleteResult}.
     */
    get isDeletable() {
        if (this.virtual()) {
            return DynamicPolygonPointDeleteResult.Virtual;
        }
        if (this._parent._dragPoint === this) {
            return DynamicPolygonPointDeleteResult.Protected;
        }
        let nonVirtualPointCount = 0;
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        for (const p of this.points(DynamicPolygonPoint.IsNotVirtual)) {
            nonVirtualPointCount++;
            if (nonVirtualPointCount > this._parent._minPoints)
                break;
        }
        // May only delete if nrOfPoints are greater then 3.
        if (!(nonVirtualPointCount > this._parent._minPoints)) {
            return DynamicPolygonPointDeleteResult.NotEnoughPoints;
        }
        // Can only delete a non virtual node if there is more than
        // three non virtual points in the polygon.
        // Try to virtually remove the point and check if the new polygon would be complex,
        // in any case move the point back.
        try {
            this._isVirtual = true;
            if (DynamicPolygonPointValidationResult.ComplexPolygon ===
                this._parent._validatePoints([...this.points(DynamicPolygonPoint.IsNotVirtual)])) {
                return DynamicPolygonPointDeleteResult.ComplexPolygon;
            }
        }
        finally {
            this._isVirtual = false;
        }
        return DynamicPolygonPointDeleteResult.Success;
    }
    /**
     * This method has two purposes.
     *
     * 1. Check if `this` point is virtual or not (if `v` is undefined). This is very useful in a UI
     * where a different visualization for a 'virtual drag point' and a 'real drag point' may be required..
     *
     * 2. Convert `this` point to/from virtual by specifying `v`.
     * @param v Optional. If not specified function only returns current state. If specified then point is converted to either a virtual point (v = `true`) or
     * a non virtual point (`v = false`).
     * @returns If `v` was not specified then `true` if point is virtual, otherwise `false`. If `v` was specified
     * it returns `true` if point was successfully converted. Otherwise `false`. A point cannot be converted from
     * non virtual to virtual if there are 3 or less non virtual points in the polygon.
     */
    virtual(v) {
        if (v === undefined) {
            return this._isVirtual;
        }
        let wasChanged = false;
        if (v !== this._isVirtual) {
            if (v) {
                // Went from not virtual drag point to virtual. This is the same thing as making the point virtual,
                // removing the existing neightbour virtual dragpoints and finally set x, y, z of this
                // to be middle between the two neighbour (non virtual) points
                if (this.isDeletable === DynamicPolygonPointDeleteResult.Success) {
                    // remove neighbour virtual points.
                    this.remove(this._next);
                    this.remove(this._previous);
                    // Move 'this' point to lie exactly in the middle between previous and current non virtual point
                    // since we removed the virtual points above the next and previous points are guaranteed to be
                    // non virtual
                    this._isVirtual = true;
                    this.moveToMiddleBetweenPointsToRef(this._previous, this._next, this);
                    wasChanged = true;
                }
            }
            else {
                // Went from virtual to not a virtual drag point. Inject two virtual points as neighbors
                this._isVirtual = false;
                this.insertVirtualPointAfter(this._previous);
                this.insertVirtualPointAfter(this);
                wasChanged = true;
            }
        }
        return wasChanged;
    }
    /**
     * Attempt to move a point to a new location on [Plane](https://doc.babylonjs.com/typedoc/classes/babylon.plane).
     * To find the location, a ray is shot from camera through `canvasCoordinate`
     * and then an intersection test is performed. If the ray intersects Plane, the point is moved to the coordinates
     * of the intersection. Otherwise nothing happens.
     * It is also not possible to move a point if it results in a complex polygon.
     * The point is assigned the same x,y,z values
     * as the coordinate where the ray intersects the plane.
     * @returns a {@link DynamicPolygonPointMoveResult}.
     */
    move(canvasCoordinate) {
        // If we get an intersection and if point is virtual then convert the virtual point to a real point.
        // and trigger the coordinate tracker. We also add two new virtual points to the coordinate tracker
        // otherwise we simply move it.
        const pickingRay = DynamicPolygonPoint._tmp.rayA;
        this._api.viewer.scene.createPickingRayToRef(canvasCoordinate.x, canvasCoordinate.y, DynamicPolygonPoint._tmp.identityMatrix, pickingRay, this._api.viewer.scene.activeCamera);
        // TODO If we move a point so it ends up perfectly on the line of the neighbour points then we could
        // convert the point to a virtual point. It no longer contributes to the polygon anyway.
        // If that happens then return DynamicPolygonPointMoveResult.ComplexPolygon
        const intersectionPoint = DynamicPolygonPoint._tmp.vector3A;
        const distance = intersectsPlaneAtToRef(pickingRay, this.plane, intersectionPoint);
        if (distance > -1) {
            const tmp = copyVertex3ToRef(this, { x: 0, y: 0, z: 0 });
            copyVertex3ToRef(intersectionPoint, this);
            if (DynamicPolygonPointValidationResult.ComplexPolygon ===
                this._parent._validatePoints([...this.points(DynamicPolygonPoint.IsNotVirtual)])) {
                copyVertex3ToRef(tmp, this);
                return DynamicPolygonPointMoveResult.ComplexPolygon;
            }
            const virtualSegment = (this._previous && this._previous.virtual()) || (this._next && this._next.virtual()) || this.virtual();
            // Only split, move and create new points if there exists virtual points (which implies a shape that can be extended). Kind of hacky
            if (virtualSegment) {
                // If points was virtual it is now not. Ensures that
                // we have a neighbour virtual point on each side.
                if (!this.virtual(false)) {
                    this._parent._dynamicPolygonPointTracker.track(this, this);
                }
                // Ensure each neighbour virtual points is located on middle of line spanning
                // this point and its neigbour non virtual points.
                this.moveToMiddleBetweenPointsToRef(this._previous._previous, this, this._previous);
                this.moveToMiddleBetweenPointsToRef(this, this._next._next, this._next);
            }
            return DynamicPolygonPointMoveResult.Success;
        }
        return DynamicPolygonPointMoveResult.RayIntersectionFailed;
    }
    /**
     * Deletes `this` point from the parent {@link DynamicPolygon}.
     * It is possible to use {@link isDeletable} to check if point can be deleted beforehand.
     * @returns a {@link DynamicPolygonPointMoveResult}.
     */
    delete() {
        const isDeletableResult = this.isDeletable;
        if (isDeletableResult !== DynamicPolygonPointDeleteResult.Success) {
            return isDeletableResult; // Not possible to delete a virtual point
        }
        // This point was not virtual but we make it virtual
        // this will remove neighbour virtual drag points
        this.virtual(true);
        return isDeletableResult;
    }
    /**
     * Enables iteration of all points in  the polygon.
     * @param predicate Optional predicate. If specified only points where the predicate returns `true` will be returned.
     * @param backwards If `true` then polygon point are visited in backwards direction. Default is `false`.
     * @returns generator.
     */
    *points(predicate, backwards = false) {
        const dir = backwards ? '_previous' : '_next';
        // eslint-disable-next-line @typescript-eslint/no-this-alias
        let current = this;
        const visited = new Set();
        while (current && !visited.has(current)) {
            visited.add(current);
            if (predicate === undefined || predicate(current)) {
                yield current;
            }
            current = current[dir];
        }
    }
    remove(p) {
        if (this._parent._head === this) {
            // Whenever a point is deleted we must check if parent.head refers to that point
            // if it does we must switch to the next point (does not matter if it is virtual or not)
            this._parent._head = p._next;
        }
        this._parent._dynamicPolygonPointTracker.untrack(p);
        // Unhook this instance from the circular list so it can be GC:ed
        // start: prev <=> p <=> next
        // end: prev <=> next
        p._previous._next = p._next;
        p._next._previous = p._previous;
        // Just make this node point to itself so GC can collect it.
        // this cycle does NOT prevent the GC from collecing it.
        p._next = p;
        p._previous = p;
    }
    insertVirtualPointAfter(p) {
        const newPoint = this._parent._virtualPointFactory();
        // start: prev <=> p <=> next
        // end: prev <=> p <=> newPoint <=> next
        newPoint._next = p._next;
        newPoint._previous = p;
        p._next._previous = newPoint;
        p._next = newPoint;
        newPoint._parent = p._parent;
        this.moveToMiddleBetweenPointsToRef(p, p._next._next, newPoint);
        return newPoint;
    }
    moveToMiddleBetweenPointsToRef(start, end, dst) {
        vertexBetweenToRef(start, end, dst);
        this._parent._dynamicPolygonPointTracker.track(dst, dst);
        return dst;
    }
}
//# sourceMappingURL=DynamicPolygonPoint.js.map