import { BimCoreApi } from '../BimCoreApi';
import { Plane } from '../loader/babylonjs-import';
import { Vertex3 } from '../math';
import { DynamicPolygonPoint, DynamicPolygonPointDeleteResult } from './DynamicPolygonPoint';
import { DynamicPolygonPointValidationResult } from './DynamicPolygonPointParent';
import { DynamicPolygonWithArbitraryPoints } from './DynamicPolygonWithArbitraryPoints';

/**
 * Represents a control point in a {@link DynamicPolygonWithArbitraryPoints}. Inherit from this class if additional functionality
 * is required.
 */
export class ArbitraryShapePoint extends DynamicPolygonPoint {
    /**
     *
     * @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.
     */
    public constructor(
        protected readonly _api: BimCoreApi,
        public readonly plane: Plane,
        isVirtual: boolean
    ) {
        super(_api, plane, isVirtual);
    }

    /**
     * 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.
     */
    public virtual(v?: boolean): boolean {
        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.
                    if (this._next !== this) {
                        this.remove(this._next);
                    }
                    if (this._previous !== this) {
                        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;
    }

    /**
     * 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}.
     */
    public delete(): DynamicPolygonPointDeleteResult {
        const isDeletableResult = this.isDeletable;

        if (isDeletableResult !== DynamicPolygonPointDeleteResult.Success) {
            return isDeletableResult; // Not possible to delete a virtual point
        }

        const nonVirtualControlPointPositions: Vertex3[] = [];
        for (const controlPoint of (this._parent as unknown as DynamicPolygonWithArbitraryPoints).controlPoints()) {
            if (!controlPoint.virtual() && controlPoint !== this) {
                nonVirtualControlPointPositions.push({
                    x: controlPoint.localX,
                    y: controlPoint.localY,
                    z: controlPoint.localZ
                });
            }
        }

        //Check if we are at the edge of a line. Deleting an edge point from a line can never cause a complex polygon.
        if (this._next !== this && this._previous !== this) {
            const validationResult = (
                this._parent as unknown as DynamicPolygonWithArbitraryPoints
            ).validateControlPoints(nonVirtualControlPointPositions, false);

            if (validationResult === DynamicPolygonPointValidationResult.ComplexPolygon) {
                return DynamicPolygonPointDeleteResult.ComplexPolygon;
            }
        }

        this.virtual(true);
        if (this._next === this) {
            (this._previous as ArbitraryShapePoint).remove(this);
        } else if (this._previous === this) {
            (this._next as ArbitraryShapePoint).remove(this);
        }

        // This point was not virtual but we make it virtual
        // this will remove neighbour virtual drag points
        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.
     * @returns generator.
     */
    public *points(predicate?: (p: ArbitraryShapePoint) => boolean): Generator<ArbitraryShapePoint, void, unknown> {
        const dir = '_next';
        // eslint-disable-next-line @typescript-eslint/no-this-alias
        let current: ArbitraryShapePoint | undefined = this._parent._head as unknown as ArbitraryShapePoint;
        const visited = new Set<ArbitraryShapePoint>();
        while (current && !visited.has(current)) {
            visited.add(current);
            if (predicate === undefined || predicate(current)) {
                yield current;
            }
            current = current[dir] as unknown as ArbitraryShapePoint;
        }
    }

    protected remove(p: DynamicPolygonPoint): void {
        if (p._parent._head === p) {
            // 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)
            if (p._next !== p) {
                p._parent._head = p._next;
            } else if (p._previous !== p) {
                p._parent._head = p._previous;
            }
        }
        this._parent._dynamicPolygonPointTracker.untrack(p);
        // Unhook this instance from the circular list so it can be GC:ed

        //if the control point next/previous points to itself that means this control point is the last in the list of control points.
        if (p._next === p) {
            p._previous._next = p._previous;
        } else if (p._previous === p) {
            p._next._previous = p._next;
        } else {
            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;
    }
}
