import { Observable, TransformNode, Matrix, Material, Plane, Mesh } from '../loader/babylonjs-import';
import { BimCoreApi } from '../BimCoreApi';
import { copyVertex3ToRef, equalsWithEpsilonVertex3, Vertex3, vertexBetweenToRef } from '../math';
import { ArbitraryShapePoint } from './ArbitraryShapePoint';
import { CoordinateTracker, TrackCoordinate2D } from './CoordinateTracker';
import { DragPointPositioningFunction, DynamicPolygon, DynamicPolygonPointFactoryHandler } from './DynamicPolygon';
import { DynamicPolygonPoint } from './DynamicPolygonPoint';
import { DynamicPolygonPointParent, DynamicPolygonPointValidationResult } from './DynamicPolygonPointParent';

export type DragPointPositioningFunctionOrIndex = DragPointPositioningFunction | number;
export interface ArbitraryShapePointFactoryHandler<Point extends ArbitraryShapePoint> {
    (api: BimCoreApi, plane: Plane, isVirtual: boolean): Point;
}
/** Wrapper of {@link DynamicPolygon} to be able to create polygons that are any arbitrary shape such as circles, arrows etc. */
export class DynamicPolygonWithArbitraryPoints<Point extends ArbitraryShapePoint = ArbitraryShapePoint>
    implements DynamicPolygonPointParent<ArbitraryShapePoint>
{
    private _controlPointsHead?: Point;
    private _previousPositions = new Map<DynamicPolygonPoint, Vertex3>();
    private _dynamicPolygon: DynamicPolygon<DynamicPolygonPoint>;
    public readonly onPointTrackableScreen: Observable<TrackCoordinate2D<Point>>;
    public dragPoint?: Point;
    public *controlPoints(
        predicate?: (p: DynamicPolygonPoint) => boolean
    ): Generator<DynamicPolygonPoint, void, unknown> {
        for (const p of this._controlPointsHead?.points(predicate) ?? []) {
            yield p;
        }
    }

    /**
     * Applies changes which may have occured since the last time apply or build was called. When a control point is moved and then called, the shape
     * of the polygon will be recalculated using the factory method provided in the constructor.
     */
    public apply(): void {
        const yieldedControlPoints = [...this.controlPoints()];

        if (this._dragPointPositioningFunctionOrIndex !== undefined && this.dragPoint) {
            const dragPointPrev = this._previousPositions.get(this.dragPoint);
            const epsilon = 0.0001;
            if (dragPointPrev && !equalsWithEpsilonVertex3(dragPointPrev, this.dragPoint, epsilon)) {
                for (let i = 0; i < yieldedControlPoints.length; i++) {
                    if (this.dragPoint !== yieldedControlPoints[i]) {
                        this._dynamicPolygon.constrainChild(this.dragPoint, yieldedControlPoints[i], dragPointPrev);
                    }
                }
            }
            if (typeof this._dragPointPositioningFunctionOrIndex !== 'number') {
                const pos = this._dragPointPositioningFunctionOrIndex(yieldedControlPoints);
                this.dragPoint.x = pos.x;
                this.dragPoint.y = pos.y;
                this.dragPoint.z = pos.z;
            }

            this._previousPositions.set(this.dragPoint, {
                x: this.dragPoint.x,
                y: this.dragPoint.y,
                z: this.dragPoint.z
            });
        }

        const nonVirtualControlPointPositions: Vertex3[] = [];
        for (const controlPoint of this.controlPoints()) {
            if (!controlPoint.virtual()) {
                nonVirtualControlPointPositions.push({
                    x: controlPoint.localX,
                    y: controlPoint.localY,
                    z: controlPoint.localZ
                });
            }
        }
        const validationResult = this.createPointsForPolygonFromControlPoints(nonVirtualControlPointPositions, false);

        if (
            validationResult === DynamicPolygonPointValidationResult.Ok /*||
            validationResult === DynamicPolygonPointValidationResult.ComplexPolygon*/
        ) {
            for (let i = 0; i < yieldedControlPoints.length; i++) {
                const controlPoint = yieldedControlPoints[i];

                this._previousPositions.set(controlPoint, {
                    x: controlPoint.x,
                    y: controlPoint.y,
                    z: controlPoint.z
                });
            }

            if (this.dragPoint) {
                this._previousPositions.set(this.dragPoint, {
                    x: this.dragPoint.x,
                    y: this.dragPoint.y,
                    z: this.dragPoint.z
                });
            }
            this._dynamicPolygon.apply();
        } else {
            throw new Error('Invalid polygon state: ' + validationResult);
        }
    }

    private assignPointProperties(p: Point): void {
        p._parent = this as unknown as DynamicPolygonPointParent<DynamicPolygonPoint>;
    }

    private createArbitraryShapePoint(p: Vertex3): Point {
        const fP = this._controlPointFactory(this._api, this._dynamicPolygon.plane, false);
        this.assignPointProperties(fP);

        if (this._dynamicPolygon.parent) {
            fP.setLocals(p.x, p.y, p.z);
        } else {
            copyVertex3ToRef(p, fP);
        }
        return fP;
    }

    private createDynamicPolygonPoint(p: Vertex3): DynamicPolygonPoint {
        const fP = this._dynamicPolygon.createPoint(false);
        fP._parent = this._dynamicPolygon;

        if (this._dynamicPolygon.parent) {
            fP.setLocals(p.x, p.y, p.z);
        } else {
            copyVertex3ToRef(p, fP);
        }
        return fP;
    }

    public constructor(
        private readonly _polygonPointsFactory: (controlPoints: Vertex3[]) => Vertex3[],
        private readonly _polygonPointsValidator:
            | ((linePoints: Vertex3[]) => DynamicPolygonPointValidationResult)
            | undefined,
        public readonly name: string,
        protected readonly _api: BimCoreApi,
        pointFactory: DynamicPolygonPointFactoryHandler<DynamicPolygonPoint>,
        private _controlPointFactory: ArbitraryShapePointFactoryHandler<Point>,
        recalculate2DEachFrame = false,
        parent?: TransformNode,
        private _dragPointPositioningFunctionOrIndex?: DragPointPositioningFunctionOrIndex
    ) {
        this._dynamicPolygonPointTracker = new CoordinateTracker<ArbitraryShapePoint>(
            this._api,
            recalculate2DEachFrame
        );
        this._dynamicPolygon = new DynamicPolygon(name, _api, pointFactory, recalculate2DEachFrame, parent);
        this.onPointTrackableScreen = this._dynamicPolygonPointTracker.onUpdateObservable as unknown as Observable<
            TrackCoordinate2D<Point>
        >;
    }
    _head?: ArbitraryShapePoint | undefined;

    /**
     * @hidden
     * @internal
     */
    _minPoints = 2;

    /**
     * @hidden
     * @internal
     */
    _dynamicPolygonPointTracker: CoordinateTracker<ArbitraryShapePoint>;

    /**
     * @hidden
     * @internal
     */
    _virtualPointFactory(): ArbitraryShapePoint {
        return this._controlPointFactory(this._api, this._dynamicPolygon.plane, true);
        // return this._dynamicPolygon.createPoint(true);
    }

    isEnabled: boolean;

    /**
     * @hidden
     * @internal
     */
    _validatePoints(points: Vertex3[]): DynamicPolygonPointValidationResult {
        if (this._polygonPointsValidator) {
            return this._polygonPointsValidator(points);
        } else {
            return DynamicPolygonPointValidationResult.Ok;
        }
    }
    worldMatrix(): Matrix {
        return this._dynamicPolygon.mesh.computeWorldMatrix();
    }

    /**
     * Sets the material of the underlying DynamicPolygon
     * @param material
     */
    public setMaterial(material: Material): void {
        this._dynamicPolygon.mesh.material = material;
    }

    /**
     * @hidden
     * @internal
     */
    public validateControlPoints(
        controlPoints: Vertex3[],
        transformPointsToLocal: boolean
    ): DynamicPolygonPointValidationResult {
        const newPolygonPoints = this._polygonPointsFactory(controlPoints);
        return this._dynamicPolygon.transformAndValidatePoints(newPolygonPoints, transformPointsToLocal);
    }

    private createPointsForPolygonFromControlPoints(
        controlPoints: Vertex3[],
        transformPointsToLocal: boolean
    ): DynamicPolygonPointValidationResult {
        // Note that the points will be generated from the factory without a real plane defined
        const newPolygonPoints = this._polygonPointsFactory(controlPoints);

        const validationResult = this._dynamicPolygon.transformAndValidatePoints(
            newPolygonPoints,
            transformPointsToLocal
        );
        if (validationResult !== DynamicPolygonPointValidationResult.Ok) {
            return validationResult;
        }

        const dynamicPolygonPoints: DynamicPolygonPoint[] = [];
        for (const p of newPolygonPoints) {
            const fP = this.createDynamicPolygonPoint(p);

            dynamicPolygonPoints.push(fP);
        }

        // Adds the first point again because the method points in DynamicPolygonPoint will not add the same point twice
        const fP = this.createDynamicPolygonPoint(newPolygonPoints[0]);
        dynamicPolygonPoints.push(fP);

        // Sets the next and previous of the new polygon points
        for (let i = 0; i < dynamicPolygonPoints.length - 1; i++) {
            const p0 = dynamicPolygonPoints[i];
            const p1 = dynamicPolygonPoints[(i + 1) % (dynamicPolygonPoints.length - 1)];

            p0._next = p1;
            p1._previous = p0;
        }

        this._dynamicPolygon.assignNewPolygonPoints(dynamicPolygonPoints);

        return validationResult;
    }

    /**
     * @returns the babylonjs mesh
     */
    public get mesh(): Mesh {
        return this._dynamicPolygon.mesh;
    }

    /**
     * Similar to build in {@link DynamicPolygon}, but instead of providing the points that define every edge of the polygon, this method takes in control points
     * which when used in together with the factory method can create arbitrary sets of points to define polygons
     * @param initialControlPoints The control points used by the factory method provided in the constructor, for example the centre and radius point of a circle
     * @param transformPointsToLocal Wheter to transform the points to the DynamicPolygon's parent local space before using them to build
     * @param includeVirtualPoints Wheter to create virtual points or not
     * @returns The DynamicPolygonPointValidationResult,
     */
    public build(
        initialControlPoints: Vertex3[],
        transformPointsToLocal = true,
        includeVirtualPoints = true
    ): DynamicPolygonPointValidationResult {
        this.clear();
        this.createPointsForPolygonFromControlPoints(initialControlPoints, transformPointsToLocal);

        const nonVirtualControlPointsForShape: Point[] = [];
        for (const p of initialControlPoints) {
            const controlPoint = this.createArbitraryShapePoint(p);
            this.assignPointProperties(controlPoint);

            nonVirtualControlPointsForShape.push(controlPoint);
        }
        this._head = nonVirtualControlPointsForShape[0];

        const virtualControlPointsForShape: Point[] = [];
        if (includeVirtualPoints) {
            for (let i = 0; i < nonVirtualControlPointsForShape.length - 1; i++) {
                const virtualPoint = this._controlPointFactory(this._api, this._dynamicPolygon.plane, true);
                this.assignPointProperties(virtualPoint);

                const p0 = nonVirtualControlPointsForShape[i];
                const p1 = nonVirtualControlPointsForShape[i + 1];
                vertexBetweenToRef(p0, p1, virtualPoint);

                p0._next = virtualPoint;
                virtualPoint._previous = p0;

                p1._previous = virtualPoint;
                virtualPoint._next = p1;

                virtualControlPointsForShape.push(virtualPoint);
            }
        } else {
            for (let i = 0; i < nonVirtualControlPointsForShape.length - 1; i++) {
                const p0 = nonVirtualControlPointsForShape[i];
                const p1 = nonVirtualControlPointsForShape[i + 1];

                p0._next = p1;
                p1._previous = p0;
            }
        }

        if (
            this._dragPointPositioningFunctionOrIndex &&
            typeof this._dragPointPositioningFunctionOrIndex !== 'number'
        ) {
            const pos = this._dragPointPositioningFunctionOrIndex(nonVirtualControlPointsForShape);
            this.dragPoint = this.createArbitraryShapePoint({ x: pos.x, y: pos.y, z: pos.z });
            this.assignPointProperties(this.dragPoint);
        } else if (this._dragPointPositioningFunctionOrIndex !== undefined) {
            this.dragPoint = nonVirtualControlPointsForShape[this._dragPointPositioningFunctionOrIndex];
        }

        this._previousPositions.clear();
        for (let i = 0; i < nonVirtualControlPointsForShape.length; i++) {
            const nonVirtualPointForShape = nonVirtualControlPointsForShape[i];
            this._previousPositions.set(nonVirtualPointForShape, {
                x: nonVirtualPointForShape.x,
                y: nonVirtualPointForShape.y,
                z: nonVirtualPointForShape.z
            });
        }

        if (this.dragPoint) {
            this._previousPositions.set(this.dragPoint, {
                x: this.dragPoint.x,
                y: this.dragPoint.y,
                z: this.dragPoint.z
            });
        }

        const controlPointsForShape = [...nonVirtualControlPointsForShape, ...virtualControlPointsForShape];

        for (const p of controlPointsForShape) {
            this._dynamicPolygonPointTracker.track(p, p);
        }

        if (this.dragPoint) {
            this._dynamicPolygonPointTracker.track(this.dragPoint, this.dragPoint);
        }

        this._controlPointsHead = controlPointsForShape[0];

        this.apply();

        return DynamicPolygonPointValidationResult.Ok;
    }

    /**
     * Disposes the {@link DynamicPolygonWithArbitraryPoints}. It is no longer useable after this call.
     */
    public dispose(): void {
        this.clear();
        this._dynamicPolygon.dispose();
    }

    /**
     * Clears the polygon of all current polygon points and control points.
     */
    public clear(): void {
        this._dynamicPolygon.clear();
        this._dynamicPolygonPointTracker.clear();
        this._controlPointsHead = undefined;
    }
}
