import { Point } from "@devexpress/utils/lib/geometry/point";

import { History } from "../../History/History";
import { Connector } from "../../Model/Connectors/Connector";
import { ConnectionPointSide, DiagramItem, ItemKey } from "../../Model/DiagramItem";
import { DiagramModel } from "../../Model/Model";
import { ModelUtils } from "../../Model/ModelUtils";
import { DiagramMouseEvent } from "../Event";
import { MouseHandler } from "../MouseHandler";
import { MouseHandlerDraggingState } from "./MouseHandlerDraggingState";
import { GeometryUtils } from "../../Utils";
import { ConnectorRenderPoint } from "../../Model/Connectors/ConnectorRenderPoint";

export class MouseHandlerMoveConnectorOrthogonalSideState extends MouseHandlerDraggingState {
    startPoint: Point;
    connector: Connector;
    canCreatePoints: boolean = true;
    leftRenderPointIndex: number;
    rightRenderPointIndex: number;
    isHorizontal: boolean;
    leftPointIndex: number;
    rightPointIndex: number;

    constructor(handler: MouseHandler, history: History, protected model: DiagramModel) {
        super(handler, history);
    }

    private saveSidePoints(markLeftRenderPointIndex: number, markRightRenderPointIndex: number): void {
        const renderPoints = this.connector.getRenderPoints(true);
        this.isHorizontal = renderPoints[markLeftRenderPointIndex].y === renderPoints[markRightRenderPointIndex].y;
        this.iterateRenderPoints(renderPoints, markLeftRenderPointIndex, false,
            (pt, i) => {
                if(pt.pointIndex !== -1)
                    this.leftPointIndex = pt.pointIndex;
                this.leftRenderPointIndex = i;
            },
            (pt) => !GeometryUtils.arePointsOfOrthogonalLine(renderPoints[markLeftRenderPointIndex], pt, this.isHorizontal));
        this.iterateRenderPoints(renderPoints, this.leftRenderPointIndex, true,
            (pt, i) => {
                if(pt.pointIndex !== -1)
                    this.rightPointIndex = pt.pointIndex;
                this.rightRenderPointIndex = i;
            },
            (pt) => !GeometryUtils.arePointsOfOrthogonalLine(renderPoints[markLeftRenderPointIndex], pt, this.isHorizontal));
    }

    private iterateRenderPoints(renderPoints: ConnectorRenderPoint[], startIndex: number, direction: boolean, callback: (renderPoint: ConnectorRenderPoint, index: number) => void, stopPredicate?: (renderPoint: ConnectorRenderPoint, index: number) => boolean) {
        for(let i = startIndex; direction ? i < renderPoints.length : i >= 0; direction ? i++ : i--) {
            const point = renderPoints[i];
            if(stopPredicate && stopPredicate(point, i)) break;
            callback(point, i);
        }
    }

    onMouseDown(evt: DiagramMouseEvent): void {
        this.startPoint = evt.modelPoint;

        this.connector = this.model.findConnector(evt.source.key);
        this.handler.addInteractingItem(this.connector);
        const renderPointIndexes = evt.source.value.split("_");
        this.saveSidePoints(parseInt(renderPointIndexes[0]), parseInt(renderPointIndexes[1]));
        super.onMouseDown(evt);
    }
    private shouldCreatePoint(isLeft: boolean): boolean {
        if(!this.canCreatePoints) return false;
        if(isLeft && (this.leftPointIndex === undefined || this.leftPointIndex === 0)) return true;
        if(!isLeft && (this.rightPointIndex === undefined || this.rightPointIndex === this.connector.points.length - 1)) return true;
        const renderPoints = this.connector.getRenderPoints(true);
        if(isLeft && !this.connector.points[this.leftPointIndex].equals(renderPoints[this.leftRenderPointIndex])) return true;
        if(!isLeft && !this.connector.points[this.rightPointIndex].equals(renderPoints[this.rightRenderPointIndex])) return true;
        return false;
    }
    onApplyChanges(evt: DiagramMouseEvent): void {
        if(this.shouldCreatePoint(true) || this.shouldCreatePoint(false)) {
            let renderPoints = this.connector.getRenderPoints(true);
            const leftRenderPoint = renderPoints[this.leftRenderPointIndex];
            const rightRenderPoint = renderPoints[this.rightRenderPointIndex];
            if(this.shouldCreatePoint(true)) {
                const leftPoint = new Point(leftRenderPoint.x, leftRenderPoint.y);
                if(this.leftPointIndex === 0) {
                    this.leftPointIndex = 1;
                    this.correctEdgePoint(leftPoint, rightRenderPoint, this.connector.beginItem, this.connector.beginConnectionPointIndex);
                }
                else if(this.leftPointIndex === undefined)
                    this.iterateRenderPoints(renderPoints, this.leftRenderPointIndex, true,
                        pt => {
                            if(pt.pointIndex !== -1)
                                this.leftPointIndex = pt.pointIndex;
                        },
                        () => this.leftPointIndex !== undefined);
                ModelUtils.addConnectorPoint(this.history, this.connector.key, this.leftPointIndex, leftPoint);
                if(this.rightPointIndex !== undefined)
                    this.rightPointIndex++;
            }
            if(this.shouldCreatePoint(false)) {
                renderPoints = this.connector.getRenderPoints(true);
                const rightPoint = new Point(rightRenderPoint.x, rightRenderPoint.y);
                if(this.rightPointIndex === this.connector.points.length - 1) {
                    this.correctEdgePoint(rightPoint, leftRenderPoint, this.connector.endItem, this.connector.endConnectionPointIndex);
                    this.rightPointIndex--;
                }
                else if(this.rightPointIndex === undefined)
                    this.iterateRenderPoints(renderPoints, this.rightRenderPointIndex, false,
                        pt => {
                            if(pt.pointIndex !== -1)
                                this.rightPointIndex = pt.pointIndex;
                        },
                        () => this.rightPointIndex === this.leftPointIndex);
                this.rightPointIndex++;
                ModelUtils.addConnectorPoint(this.history, this.connector.key, this.rightPointIndex, rightPoint);
            }
        }
        this.canCreatePoints = false;

        const point = this.getSnappedPoint(evt, evt.modelPoint);
        ModelUtils.moveConnectorRightAnglePoints(this.history, this.connector, this.leftPointIndex, this.rightPointIndex,
            this.isHorizontal ? undefined : point.x,
            this.isHorizontal ? point.y : undefined);
        this.handler.tryUpdateModelSize();
    }

    onFinishWithChanges(): void {
        ModelUtils.deleteConnectorUnnecessaryPoints(this.history, this.connector);
        ModelUtils.fixConnectorBeginEndConnectionIndex(this.history, this.connector);
        this.handler.tryUpdateModelSize();
    }

    correctEdgePoint(point: Point, directionPoint: Point, item: DiagramItem, connectionPointIndex: number): void {
        let offset = 0;
        if(item) {
            const side = item.getConnectionPointSideByIndex(connectionPointIndex);
            const rect = item.rectangle;
            offset = Connector.minOffset;
            switch(side) {
                case ConnectionPointSide.South:
                    offset += rect.bottom - point.y;
                    break;
                case ConnectionPointSide.North:
                    offset += point.y - rect.y;
                    break;
                case ConnectionPointSide.East:
                    offset += rect.right - point.x;
                    break;
                case ConnectionPointSide.West:
                    offset += point.x - rect.x;
                    break;
            }
        }
        if(this.isHorizontal)
            if(point.x > directionPoint.x)
                point.x -= Math.min(offset, point.x - directionPoint.x);
            else
                point.x += Math.min(offset, directionPoint.x - point.x);
        else if(point.y > directionPoint.y)
            point.y -= Math.min(offset, point.y - directionPoint.y);
        else
            point.y += Math.min(offset, directionPoint.y - point.y);
    }

    getDraggingElementKeys(): ItemKey[] {
        return [this.connector.key];
    }
}
