import { UnitConverter } from "@devexpress/utils/lib/class/unit-converter";
import { Point } from "@devexpress/utils/lib/geometry/point";
import { Rectangle } from "@devexpress/utils/lib/geometry/rectangle";
import { Segment } from "@devexpress/utils/lib/geometry/segment";
import { Size } from "@devexpress/utils/lib/geometry/size";

import { PAGE_BG_TEXTFLOOR_FILTER_IDPREFIX } from "../../../src/Render/CanvasManagerBase";
import { INativeConnector } from "../../Api/INativeItem";
import { NativeConnector } from "../../Api/NativeItem";
import { DiagramUnit } from "../../Enums";
import { MouseEventElementType } from "../../Events/Event";
import { TextOwner } from "../../Render/Measurer/ITextMeasurer";
import {
    PathPrimitive, PathPrimitiveLineToCommand, PathPrimitiveMoveToCommand
} from "../../Render/Primitives/PathPrimitive";
import { SvgPrimitive } from "../../Render/Primitives/Primitive";
import { TextPrimitive } from "../../Render/Primitives/TextPrimitive";
import { RenderUtils } from "../../Render/Utils";
import { ConnectorRoutingMode } from "../../Settings";
import { GeometryUtils } from "../../Utils";
import { ConnectionPoint } from "../ConnectionPoint";
import { ConnectionPointSide, DiagramItem } from "../DiagramItem";
import { ModelUtils } from "../ModelUtils";
import { ConnectorPointsCalculator } from "./Calculators/ConnectorPointsCalculator";
import { ConnectorPointsCalculatorBase } from "./Calculators/ConnectorPointsCalculatorBase";
import {
    ConnectorPointsOrthogonalCalculator
} from "./Calculators/ConnectorPointsOrthogonalCalculator";
import {
    ConnectorLineEndingArrowStrategy, ConnectorLineEndingFilledTriangleStrategy,
    ConnectorLineEndingNoneStrategy, ConnectorLineEndingOutlinedTriangleStrategy,
    ConnectorLineEndingStrategy
} from "./ConnectorLineEndingStrategies";
import {
    ConnectorLineEnding, ConnectorLineOption, ConnectorProperties
} from "./ConnectorProperties";
import { ConnectorRenderPoint } from "./ConnectorRenderPoint";
import { ConnectorText, ConnectorTexts } from "./ConnectorTexts";
import { ConnectorRenderPointsContext } from "./Routing/ConnectorRenderPointsContext";
import { IConnectorRoutingStrategy } from "./Routing/ConnectorRoutingModel";

export enum ConnectorPosition { Begin, End }

export const CONNECTOR_DEFAULT_TEXT_POSITION = 0.5;

export class Connector extends DiagramItem {
    points: Point[];
    beginItem: DiagramItem;
    beginConnectionPointIndex: number = -1;
    endItem: DiagramItem;
    endConnectionPointIndex: number = -1;
    texts: ConnectorTexts;
    properties: ConnectorProperties;
    private routingStrategy : IConnectorRoutingStrategy;

    static minOffset: number = UnitConverter.pixelsToTwips(24);
    static minTextHeight: number = UnitConverter.pixelsToTwips(12);

    private renderPoints: ConnectorRenderPoint[];
    private renderPointsWithoutSkipped: ConnectorRenderPoint[];
    private lockCreateRenderPoints: boolean;
    private shouldInvalidateRenderPoints: boolean;
    private actualRoutingMode : ConnectorRoutingMode;

    constructor(points: Point[]) {
        super();

        this.properties = new ConnectorProperties();
        this.points = points.map(pt => pt.clone());
        if(points.length < 2)
            throw Error("Points count should be greater than 1");
        this.texts = new ConnectorTexts();
    }

    get rectangle(): Rectangle {
        return GeometryUtils.createRectagle(this.getRenderPoints(true));
    }
    get skippedRenderPoints(): ConnectorRenderPoint[] {
        return this.renderPoints ? this.renderPoints.filter(p => p.skipped) : undefined;
    }
    private get shouldChangeRenderPoints(): boolean {
        return this.renderPoints !== undefined && this.routingStrategy !== undefined;
    }
    assign(item: Connector): void {
        super.assign(item);

        item.beginItem = this.beginItem;
        item.beginConnectionPointIndex = this.beginConnectionPointIndex;
        item.endItem = this.endItem;
        item.endConnectionPointIndex = this.endConnectionPointIndex;
        item.properties = this.properties.clone();
        item.texts = this.texts.clone();
        if(this.routingStrategy !== undefined)
            item.routingStrategy = this.routingStrategy.clone();
        if(this.renderPoints !== undefined)
            item.renderPoints = this.renderPoints.map(p => p.clone());
        if(this.renderPointsWithoutSkipped !== undefined)
            item.renderPointsWithoutSkipped = this.renderPointsWithoutSkipped.map(p => p.clone());
        if(this.actualRoutingMode !== undefined)
            item.actualRoutingMode = this.actualRoutingMode;
        if(this.lockCreateRenderPoints !== undefined)
            item.lockCreateRenderPoints = this.lockCreateRenderPoints;
        if(this.shouldInvalidateRenderPoints !== undefined)
            item.shouldInvalidateRenderPoints = this.shouldInvalidateRenderPoints;
    }
    clone(): Connector {
        const clone = new Connector(this.points);
        this.assign(clone);
        return clone;
    }

    getTextCount(): number {
        return this.texts.count();
    }
    getText(position: number = CONNECTOR_DEFAULT_TEXT_POSITION): string {
        const textObj = this.texts.get(position);
        return textObj ? textObj.value : "";
    }
    setText(text: string, position: number = CONNECTOR_DEFAULT_TEXT_POSITION): void {
        if(!text || text === "")
            this.texts.remove(position);
        else
            this.texts.set(position, new ConnectorText(position, text));
    }
    getTextPoint(position: number): Point {
        const points = this.getRenderPoints();
        return GeometryUtils.getPathPointByPosition(points, position)[0];
    }
    getTextPositionByPoint(point: Point): number {
        const points = this.getRenderPoints();
        const length = GeometryUtils.getPathLength(points);
        const pos = GeometryUtils.getPathPositionByPoint(points, point);
        const minTextHeight = UnitConverter.pointsToTwips(parseInt(this.styleText["font-size"]));
        if(minTextHeight > pos * length)
            return minTextHeight / length;
        if(minTextHeight > length - pos * length)
            return (length - minTextHeight) / length;
        return pos;
    }
    getTextRectangle(position: number): Rectangle {
        return Rectangle.fromGeometry(this.getTextPoint(position), new Size(0, 0));
    }
    changeRoutingStrategy(strategy: IConnectorRoutingStrategy): void {
        this.routingStrategy = strategy;
        this.invalidateRenderPoints();
    }
    clearRoutingStrategy() : void {
        delete this.routingStrategy;
        delete this.renderPoints;
        delete this.renderPointsWithoutSkipped;
        delete this.lockCreateRenderPoints;
        delete this.actualRoutingMode;
        delete this.shouldInvalidateRenderPoints;
        this.invalidateRenderPoints();
    }
    getCustomRenderPoints(keepSkipped: boolean = false) : ConnectorRenderPoint[] {
        const renderPoints = this.getRenderPoints(keepSkipped);
        const result : ConnectorRenderPoint[] = [];
        renderPoints.forEach((p, index) => {
            if(index > 0 && index < renderPoints.length - 1)
                result.push(p);
        });
        return result;
    }
    getRenderPoints(keepSkipped: boolean = false): ConnectorRenderPoint[] {
        if(this.shouldInvalidateRenderPoints === undefined || this.shouldInvalidateRenderPoints) {
            this.shouldInvalidateRenderPoints = false;
            if(!this.routingStrategy || !this.lockCreateRenderPoints)
                this.changeRenderPoints(this.getCalculator().getPoints());
            if(this.routingStrategy && !this.lockCreateRenderPoints && this.actualRoutingMode !== ConnectorRoutingMode.None && this.points && this.renderPoints) {
                const beginPoint = this.points[0];
                const endPoint = this.points[this.points.length - 1];
                if(!beginPoint.equals(endPoint)) {
                    const newRenderPoints = this.routingStrategy.createRenderPoints(
                        this.points, this.renderPoints, this.beginItem, this.endItem,
                        this.beginConnectionPointIndex, this.endConnectionPointIndex,
                        ModelUtils.getConnectorContainer(this));
                    if(newRenderPoints) {
                        this.changeRenderPoints(newRenderPoints);
                        this.actualRoutingMode = ConnectorRoutingMode.AllShapesOnly;
                    }
                    else
                        this.actualRoutingMode = ConnectorRoutingMode.None;
                }
            }
        }
        return keepSkipped ? this.renderPoints : this.renderPointsWithoutSkipped;
    }

    tryCreateRenderPointsContext(forceCreate?: boolean): ConnectorRenderPointsContext | undefined {
        return forceCreate || this.shouldChangeRenderPoints ? new ConnectorRenderPointsContext(this.renderPoints.map(p=>p.clone()), this.lockCreateRenderPoints, this.actualRoutingMode) : undefined;
    }
    updatePointsOnPageResize(offsetX: number, offsetY: number): void {
        this.points = this.points.map(p => p.clone().offset(offsetX, offsetY));
        if(this.renderPoints)
            this.changeRenderPoints(this.renderPoints.map(p => {
                const result = p.clone().offset(offsetX, offsetY);
                result.pointIndex = p.pointIndex;
                result.skipped = p.skipped;
                return result;
            }));

    }
    addPoint(pointIndex: number, point: Point): void {
        this.points.splice(pointIndex, 0, point);
    }
    deletePoint(pointIndex: number): void {
        this.points.splice(pointIndex, 1);
    }
    movePoint(pointIndex: number, point: Point): void {
        this.points[pointIndex] = point;
    }
    onAddPoint(pointIndex: number, point: Point): void {
        if(this.shouldChangeRenderPoints)
            this.replaceRenderPointsCore(this.routingStrategy.onAddPoint(this.points, pointIndex, point, this.renderPoints), true, ConnectorRoutingMode.AllShapesOnly);
        else
            this.invalidateRenderPoints();
    }
    onDeletePoint(pointIndex: number): void {
        if(this.shouldChangeRenderPoints)
            this.replaceRenderPointsCore(this.routingStrategy.onDeletePoint(this.points, pointIndex, this.renderPoints), this.points.length > 2, ConnectorRoutingMode.AllShapesOnly);
        else
            this.invalidateRenderPoints();
    }
    onMovePoint(pointIndex: number, point: Point) : void {
        if(this.shouldChangeRenderPoints) {
            if(pointIndex === 0 || pointIndex === this.points.length - 1)
                this.lockCreateRenderPoints = false;
            this.replaceRenderPointsCore(this.routingStrategy.onMovePoint(this.points, pointIndex, point, this.renderPoints), this.lockCreateRenderPoints, ConnectorRoutingMode.AllShapesOnly);
        }
        else
            this.invalidateRenderPoints();
    }
    onMovePoints(beginPointIndex: number, lastPointIndex: number, points: Point[]) : void {
        if(this.shouldChangeRenderPoints) {
            if(beginPointIndex === 0 || lastPointIndex === this.points.length - 1)
                this.lockCreateRenderPoints = false;
            this.replaceRenderPointsCore(this.routingStrategy.onMovePoints(this.points, beginPointIndex, lastPointIndex, points, this.renderPoints), this.lockCreateRenderPoints, ConnectorRoutingMode.AllShapesOnly);
        }
        else
            this.invalidateRenderPoints();
    }
    replaceRenderPoints(context?: ConnectorRenderPointsContext, shouldInvalidateRenderPoints?: boolean): void {
        if(context !== undefined) {
            this.replaceRenderPointsCore(context.renderPoints, context.lockCreateRenderPoints, context.actualRoutingMode);
            if(shouldInvalidateRenderPoints !== undefined)
                this.shouldInvalidateRenderPoints = shouldInvalidateRenderPoints;
        }
        else
            this.invalidateRenderPoints();
    }
    clearRenderPoints(): void {
        this.changeRenderPoints(undefined);
        this.lockCreateRenderPoints = false;
        this.actualRoutingMode = undefined;
        this.invalidateRenderPoints();
    }
    unlockCreateRenderPoints(): void {
        this.lockCreateRenderPoints = false;
    }

    private replaceRenderPointsCore(renderPoints: ConnectorRenderPoint[], lockCreateRenderPoints: boolean, mode: ConnectorRoutingMode): void {
        this.changeRenderPoints(renderPoints);
        this.lockCreateRenderPoints = lockCreateRenderPoints;
        this.actualRoutingMode = mode;
        this.invalidateRenderPoints();
    }
    private changeRenderPoints(renderPoints: ConnectorRenderPoint[]) {
        this.renderPoints = renderPoints;
        this.renderPointsWithoutSkipped = renderPoints ? this.renderPoints.filter(pt => !pt.skipped) : undefined;
    }

    getCalculator(): ConnectorPointsCalculatorBase {
        return (this.properties.lineOption === ConnectorLineOption.Straight) ?
            new ConnectorPointsCalculator(this) :
            new ConnectorPointsOrthogonalCalculator(this);
    }
    invalidateRenderPoints() : void {
        this.shouldInvalidateRenderPoints = true;
    }

    createPrimitives(instanceId: string): SvgPrimitive<SVGGraphicsElement>[] {
        let result = [];
        const points = this.getRenderPoints();

        const path = new PathPrimitive(
            points.map((pt, index) => {
                return index === 0 ? new PathPrimitiveMoveToCommand(pt.x, pt.y) : new PathPrimitiveLineToCommand(pt.x, pt.y);
            }), this.style);
        result.push(path);
        result = result.concat(this.createLineEndingPrimitives(points, path));
        result = result.concat(this.createTextPrimitives(instanceId));
        return result;
    }
    createLineEndingPrimitives(points: Point[], connectorPath: PathPrimitive): SvgPrimitive<SVGGraphicsElement>[] {
        const result = [];
        if(points.length > 1) {
            const lineEndingInfo = [
                { strategy: this.createLineEndingStrategy(this.properties.startLineEnding), point1: points[0], point2: points[1] },
                { strategy: this.createLineEndingStrategy(this.properties.endLineEnding), point1: points[points.length - 1], point2: points[points.length - 2] }
            ];
            lineEndingInfo.forEach(info => {
                const strategy = info.strategy;
                if(strategy.hasCommands()) {
                    let lineEndingPath = connectorPath;
                    if(strategy.needCreateSeparatePrimitive())
                        result.push(lineEndingPath = strategy.createPrimitive());
                    lineEndingPath.commands = lineEndingPath.commands.concat(strategy.createCommands(info.point1, info.point2));
                }
            });
        }
        return result;
    }
    createLineEndingStrategy(lineEnding: ConnectorLineEnding): ConnectorLineEndingStrategy {
        switch(lineEnding) {
            case ConnectorLineEnding.None:
                return new ConnectorLineEndingNoneStrategy(this.style);
            case ConnectorLineEnding.Arrow:
                return new ConnectorLineEndingArrowStrategy(this.style);
            case ConnectorLineEnding.OutlinedTriangle:
                return new ConnectorLineEndingOutlinedTriangleStrategy(this.style);
            case ConnectorLineEnding.FilledTriangle:
                return new ConnectorLineEndingFilledTriangleStrategy(this.style);
            default:
                return new ConnectorLineEndingStrategy(this.style);
        }
    }
    createSelectorPrimitives(): SvgPrimitive<SVGGraphicsElement>[] {
        const result = [];
        const points = this.getRenderPoints();
        result.push(new PathPrimitive(
            points.map((pt, index) => {
                if(index === 0)
                    return new PathPrimitiveMoveToCommand(pt.x, pt.y);
                else
                    return new PathPrimitiveLineToCommand(pt.x, pt.y);
            }), null, "selector"
        ));
        return result;
    }
    createTextPrimitives(instanceId: string): SvgPrimitive<SVGGraphicsElement>[] {
        if(!this.enableText) return [];

        let result = [];
        this.texts.forEach(textObj => {
            const text = this.getText(textObj.position);
            if(text && text !== "") {
                const pt = this.getTextPoint(textObj.position);
                result = result.concat([
                    new TextPrimitive(pt.x, pt.y, text, TextOwner.Connector, undefined, undefined, undefined, this.styleText, true, null, PAGE_BG_TEXTFLOOR_FILTER_IDPREFIX + instanceId, undefined, el => {
                        RenderUtils.setElementEventData(el, MouseEventElementType.ConnectorText, this.key,
                            textObj.position);
                    })
                ]);
            }
        });
        return result;
    }

    getExtremeItem(position: ConnectorPosition): DiagramItem {
        if(position === ConnectorPosition.Begin)
            return this.beginItem;
        if(position === ConnectorPosition.End)
            return this.endItem;
        return null;
    }
    getExtremeConnectionPointIndex(position: ConnectorPosition): number {
        if(position === ConnectorPosition.Begin)
            return this.beginConnectionPointIndex;
        if(position === ConnectorPosition.End)
            return this.endConnectionPointIndex;
        return -1;
    }
    getMinX(): number {
        const points = this.getRenderPoints();
        const xarr = points.map(p => p.x);
        return xarr.reduce((prev, cur) => Math.min(prev, cur), Number.MAX_VALUE);
    }
    getMinY(): number {
        const points = this.getRenderPoints();
        const yarr = points.map(p => p.y);
        return yarr.reduce((prev, cur) => Math.min(prev, cur), Number.MAX_VALUE);
    }
    getConnectionPoints(): ConnectionPoint[] {
        return [];
    }
    getConnectionPointSide(point: ConnectionPoint, targetPoint?: Point): ConnectionPointSide {
        return ConnectionPointSide.Undefined;
    }
    getSegments(): Segment<ConnectorRenderPoint>[] {
        const result = [];
        const renderPoints = this.getRenderPoints();
        renderPoints.forEach((pt, index) => {
            if(index > 0)
                result.push(new Segment(renderPoints[index - 1], pt));
        });
        return result;
    }
    intersectedByRect(rect: Rectangle): boolean {
        return this.getSegments().some(s => s.isIntersectedByRect(rect));
    }
    toNative(units?: DiagramUnit): INativeConnector {
        const item = new NativeConnector(this.key, this.dataKey);
        item.fromKey = this.beginItem && this.beginItem.dataKey;
        item.toKey = this.endItem && this.endItem.dataKey;
        item.texts = this.texts.map(t => t).sort((a, b) => a.position - b.position).map(a => a.value);
        item.fromId = this.beginItem && this.beginItem.key;
        item.fromPointIndex = this.beginConnectionPointIndex;
        item.toId = this.endItem && this.endItem.key;
        item.toPointIndex = this.endConnectionPointIndex;
        item.points = this.points.map(pt => pt.clone());
        item.applyUnits(units);
        return item;
    }
}
