import { PAGE_BG_TEXTFLOOR_FILTER_IDPREFIX } from "./CanvasManagerBase";
import { GroupPrimitive } from "./Primitives/GroupPrimitive";
import { RenderUtils } from "./Utils";
import { IVisualizersListener } from "../Events/EventManager";
import { Selection, ISelectionChangesListener } from "../Selection/Selection";
import { IMouseOperationsListener, MouseEventElementType, ITextInputOperationListener, ResizeEventSource } from "../Events/Event";
import { ItemKey, ConnectionPointSide, DiagramItem, ItemsMap } from "../Model/DiagramItem";
import { GeometryUtils } from "../Utils";
import { Rectangle } from "@devexpress/utils/lib/geometry/rectangle";
import { Size } from "@devexpress/utils/lib/geometry/size";
import { Point } from "@devexpress/utils/lib/geometry/point";
import { ConnectionPointInfo } from "../Events/Visualizers/ConnectionPointsVisualizer";
import { ExtensionLine, ExtensionLineType } from "../Events/Visualizers/ExtensionLinesVisualizer";
import { Shape } from "../Model/Shapes/Shape";
import { Connector } from "../Model/Connectors/Connector";
import { ConnectorLineOption } from "../Model/Connectors/ConnectorProperties";
import { RectanglePrimitive } from "./Primitives/RectaglePrimitive";
import { SvgPrimitive } from "./Primitives/Primitive";
import { PathPrimitive, PathPrimitiveMoveToCommand, PathPrimitiveLineToCommand, PathPrimitiveCommand } from "./Primitives/PathPrimitive";
import { TextPrimitive } from "./Primitives/TextPrimitive";
import { EllipsePrimitive } from "./Primitives/EllipsePrimitive";
import { IZoomChangesListener, IReadOnlyChangesListener } from "../Settings";
import { ShapeParameterPoint } from "../Model/Shapes/ShapeParameterPoint";
import { ConnectorRenderPoint } from "../Model/Connectors/ConnectorRenderPoint";
import { TextStyle, Style, StrokeStyle } from "../Model/Style";
import { ItemChange } from "../Model/ModelChange";
import { ICanvasViewListener } from "./CanvasViewManager";
import { UnitConverter } from "@devexpress/utils/lib/class/unit-converter";
import { TextOwner } from "./Measurer/ITextMeasurer";
import { DOMManipulator } from "./DOMManipulator";
import { Browser } from "@devexpress/utils/lib/browser";
import { DomUtils } from "@devexpress/utils/lib/utils/dom";
import { NOT_VALID_CSSCLASS } from "./CanvasItemsManager";
import { CanvasManager } from "./CanvasManager";
import { ConnectionTargetInfo } from "../Events/Visualizers/ConnectionTargetVisualizer";
import { ContainerTargetInfo } from "../Events/Visualizers/ContainerTargetVisualizer";
import { Metrics } from "@devexpress/utils/lib/geometry/metrics";
import { MathUtils } from "@devexpress/utils/lib/utils/math";
import { ModelUtils } from "../Model/ModelUtils";

const MULTIPLE_SELECTION_KEY = "-1";
export const SELECTION_ELEMENT_CLASSNAMES = {
    SELECTION_RECTANGLE: "selection-rect",
    CONNECTION_POINT: "connection-point",
    ACTIVE: "active",
    CONTAINER_TARGET: "container-target",
    CONNECTION_TARGET: "connection-target",
    EXTENSION_LINE: "extension-line",
    CONNECTION_MARK: "connection-mark",
    SELECTION_MARK: "selection-mark",
    LOCKED_SELECTION_MARK: "locked-selection-mark",
    ITEMS_SELECTION_RECT: "items-selection-rect",
    CONNECTOR_MULTI_SELECTION: "connector-multi-selection",
    CONNECTOR_SELECTION: "connector-selection",
    CONNECTOR_POINT_MARK: "connector-point-mark",
    CONNECTOR_SELECTION_MASK: "connector-selection-mask",
    CONNECTOR_SIDE_MARK: "connector-side-mark",
    ITEM_SELECTION_RECT: "item-selection-rect",
    ITEM_MULTI_SELECTION: "item-multi-selection-rect"
};
export const ACTIVE_SELECTION_CSSCLASS = "dxdi-active-selection";

export class CanvasSelectionManager extends CanvasManager implements IVisualizersListener, IMouseOperationsListener,
    ITextInputOperationListener, ISelectionChangesListener, ICanvasViewListener, IReadOnlyChangesListener {
    private itemSelectionContainer: SVGElement;
    private selectionMarksContainer: SVGElement;
    private visualizersContainer: SVGElement;
    private selectionRectElement: SVGElement;
    private resizeInfoElement: SVGElement;
    private connectionPointElements: SVGElement[] = [];
    private connectionTargetElement: SVGElement;
    private containerTargetElement: SVGElement;
    private extensionLineElements: SVGElement[] = [];

    private parentContainer: SVGElement;

    private selectionMap: { [shapeKey: string]: CanvasElement } = {};

    static selectionMarkSize: number = UnitConverter.pixelsToTwips(10);
    static lockedSelectionMarkSize: number = UnitConverter.pixelsToTwips(8);
    static selectionOffset: number = UnitConverter.pixelsToTwips(2);
    static selectionRectLineWidth: number = UnitConverter.pixelsToTwips(1);
    static multiSelectionRectLineWidth: number = UnitConverter.pixelsToTwips(1);
    static connectionPointSmallSize: number = UnitConverter.pixelsToTwips(5);
    static connectionPointLargeSize: number = UnitConverter.pixelsToTwips(12);
    static connectionPointShift: number = UnitConverter.pixelsToTwips(16);
    static connectionTargetBorderWidth: number = UnitConverter.pixelsToTwips(2);
    static geomertyMarkSize: number = UnitConverter.pixelsToTwips(8);
    static connectorPointMarkSize: number = UnitConverter.pixelsToTwips(6);
    static connectorSideMarkSize: number = UnitConverter.pixelsToTwips(6);
    static extensionLineWidth: number = UnitConverter.pixelsToTwips(1);
    static extensionLineOffset: number = UnitConverter.pixelsToTwips(1);
    static extensionLineEndingSize: number = UnitConverter.pixelsToTwips(6);
    static resizeInfoOffset: number = UnitConverter.pixelsToTwips(16);
    static resizeInfoTextOffset: number = UnitConverter.pixelsToTwips(2);
    static resizeInfoLineWidth: number = UnitConverter.pixelsToTwips(1);
    static evenOddSelectionCorrection = UnitConverter.pixelsToTwips(1);

    constructor(parent: SVGElement, zoomLevel: number, private readOnly: boolean, dom: DOMManipulator, instanceId: string) {
        super(zoomLevel, dom, instanceId);
        this.parentContainer = parent;
        this.initializeContainerElements(parent);
    }
    private initializeContainerElements(parent: SVGElement) {
        this.itemSelectionContainer = this.createAndChangePrimitiveElement(
            new GroupPrimitive([], null), parent
        );
        this.visualizersContainer = this.createAndChangePrimitiveElement(
            new GroupPrimitive([], null), parent
        );
        this.selectionMarksContainer = this.createAndChangePrimitiveElement(
            new GroupPrimitive([], null), parent
        );
    }
    clear() {
        RenderUtils.removeContent(this.itemSelectionContainer);
        RenderUtils.removeContent(this.selectionMarksContainer);
        RenderUtils.removeContent(this.visualizersContainer);
        this.selectionRectElement = undefined;
        this.resizeInfoElement = undefined;
        this.connectionPointElements = [];
        this.connectionTargetElement = undefined;
        this.containerTargetElement = undefined;
        this.extensionLineElements = [];
        this.selectionMap = {};
    }

    private showSelectionRect(rect: Rectangle) {
        DomUtils.addClassName(this.parentContainer, ACTIVE_SELECTION_CSSCLASS);
        const primitive = new RectanglePrimitive(rect.x, rect.y, rect.width, rect.height,
            StrokeStyle.default1pxInstance, SELECTION_ELEMENT_CLASSNAMES.SELECTION_RECTANGLE);
        const rectEl = this.getSelectionRectElement(primitive);
        this.changePrimitiveElement(primitive, rectEl);
    }
    private hideSelectionRect() {
        DomUtils.removeClassName(this.parentContainer, ACTIVE_SELECTION_CSSCLASS);
        if(this.selectionRectElement !== undefined)
            this.dom.changeByFunc(this.selectionRectElement, e => e.style.display = "none");
    }
    private getSelectionRectElement(primitive: SvgPrimitive<SVGElement>) {
        if(this.selectionRectElement !== undefined)
            this.dom.changeByFunc(this.selectionRectElement, e => e.style.display = "");
        else
            this.selectionRectElement = this.createPrimitiveElement(primitive, this.visualizersContainer);
        return this.selectionRectElement;
    }
    showResizeInfo(point: Point, text: string) {
        const rectPrimitive = new RectanglePrimitive(point.x, point.y, 0, 0, StrokeStyle.default1pxInstance);
        const primitive = new GroupPrimitive([
            rectPrimitive,
            new TextPrimitive(point.x, point.y, text, TextOwner.Resize)
        ], "resize-info");
        const groupEl = this.getResizeInfoElement(primitive);
        this.changePrimitiveElement(primitive, groupEl);
        const textSize = this.dom.measurer.measureTextLine(text, null, TextOwner.Resize).applyConverter(UnitConverter.pixelsToTwips);
        rectPrimitive.width = textSize.width + CanvasSelectionManager.resizeInfoTextOffset * 2;
        rectPrimitive.height = textSize.height + CanvasSelectionManager.resizeInfoTextOffset * 2;
        rectPrimitive.x = point.x - textSize.width / 2 - CanvasSelectionManager.resizeInfoTextOffset;
        rectPrimitive.y = point.y - textSize.height / 2 - CanvasSelectionManager.resizeInfoTextOffset;
        this.changePrimitiveElement(primitive, groupEl);
    }
    hideResizeInfo() {
        if(this.resizeInfoElement !== undefined)
            this.dom.changeByFunc(this.resizeInfoElement, e => e.style.display = "none");
    }
    private getResizeInfoElement(primitive: SvgPrimitive<SVGElement>) {
        if(this.resizeInfoElement !== undefined)
            this.dom.changeByFunc(this.resizeInfoElement, e => e.style.display = "");
        else
            this.resizeInfoElement = this.createPrimitiveElement(primitive, this.visualizersContainer);
        return this.resizeInfoElement;
    }
    getConnectionPointClassName(active: boolean, allowed: boolean): string {
        let className = SELECTION_ELEMENT_CLASSNAMES.CONNECTION_POINT;
        if(active)
            className += " " + SELECTION_ELEMENT_CLASSNAMES.ACTIVE;
        if(!allowed)
            className += " " + NOT_VALID_CSSCLASS;

        return className;
    }
    showConnectionPoint(index: number, point: Point, key: ItemKey, value: any, active: boolean, allowed: boolean): void {
        this.showConnectionPointCore(index * 2, point.x, point.y,
            CanvasSelectionManager.connectionPointLargeSize, CanvasSelectionManager.connectionPointLargeSize,
            MouseEventElementType.ShapeConnectionPoint, key, value, SELECTION_ELEMENT_CLASSNAMES.CONNECTION_POINT + " selector" + (!allowed ? " " + NOT_VALID_CSSCLASS : ""));
        this.showConnectionPointCore(index * 2 + 1, point.x, point.y,
            CanvasSelectionManager.connectionPointSmallSize, CanvasSelectionManager.connectionPointSmallSize,
            MouseEventElementType.ShapeConnectionPoint, key, value, this.getConnectionPointClassName(active, allowed));
    }
    showConnectionPointCore(index: number, cx: number, cy: number, rx: number, ry: number,
        type: MouseEventElementType, key: ItemKey, value: any, className: string) {
        const primitive = new EllipsePrimitive(cx, cy, rx, ry, null, className, e => RenderUtils.setElementEventData(e, type, key, value));
        const ellEl = this.getConnectionPointElement(primitive, index);
        this.changePrimitiveElement(primitive, ellEl);
    }
    private hideConnectionPoints() {
        for(let i = 0; i < this.connectionPointElements.length; i++)
            this.dom.changeByFunc(this.connectionPointElements[i], e => e.style.display = "none");
    }
    private getConnectionPointElement(primitive: SvgPrimitive<SVGElement>, index: number) {
        let ellEl = this.connectionPointElements[index];
        if(ellEl !== undefined)
            this.dom.changeByFunc(ellEl, e => e.style.display = "");
        else {
            ellEl = this.createPrimitiveElement(primitive, this.visualizersContainer);
            this.connectionPointElements[index] = ellEl;
        }
        return ellEl;
    }
    private showContainerTarget(index: number, targetRect: Rectangle) {
        const primitive = new RectanglePrimitive(targetRect.x, targetRect.y, targetRect.width, targetRect.height, null, SELECTION_ELEMENT_CLASSNAMES.CONTAINER_TARGET);
        const rectEl = this.getContainerTargetElement(primitive);
        this.changePrimitiveElement(primitive, rectEl);
    }
    private hideContainerTarget() {
        if(this.containerTargetElement)
            this.dom.changeByFunc(this.containerTargetElement, e => e.style.display = "none");
    }
    private getContainerTargetElement(primitive: SvgPrimitive<SVGElement>) {
        if(this.containerTargetElement !== undefined)
            this.dom.changeByFunc(this.containerTargetElement, e => e.style.display = "");
        else
            this.containerTargetElement = this.createPrimitiveElement(primitive, this.itemSelectionContainer);
        return this.containerTargetElement;
    }
    private showConnectionTarget(index: number, targetRect: Rectangle) {
        const primitive = new RectanglePrimitive(targetRect.x, targetRect.y, targetRect.width, targetRect.height, null, SELECTION_ELEMENT_CLASSNAMES.CONNECTION_TARGET);
        const rectEl = this.getConnectionTargetElement(primitive);
        this.changePrimitiveElement(primitive, rectEl);
    }
    private hideConnectionTarget() {
        if(this.connectionTargetElement)
            this.dom.changeByFunc(this.connectionTargetElement, e => e.style.display = "none");
    }
    private getConnectionTargetElement(primitive: SvgPrimitive<SVGElement>) {
        if(this.connectionTargetElement !== undefined)
            this.dom.changeByFunc(this.connectionTargetElement, e => e.style.display = "");
        else
            this.connectionTargetElement = this.createPrimitiveElement(primitive, this.itemSelectionContainer);
        return this.connectionTargetElement;
    }
    private showExtensionLine(index: number, type: ExtensionLineType, startPoint: Point, endPoint: Point, text: string) {
        let className = SELECTION_ELEMENT_CLASSNAMES.EXTENSION_LINE;
        if(type === ExtensionLineType.VerticalCenterAfter || type === ExtensionLineType.VerticalCenterBefore ||
            type === ExtensionLineType.HorizontalCenterAbove || type === ExtensionLineType.HorizontalCenterBelow)
            className += " center";
        if(type === ExtensionLineType.VerticalCenterToPageCenter || type === ExtensionLineType.HorizontalCenterToPageCenter ||
            type === ExtensionLineType.LeftToPageCenter || type === ExtensionLineType.RightToPageCenter ||
            type === ExtensionLineType.TopToPageCenter || type === ExtensionLineType.BottomToPageCenter)
            className += " page";

        let x1_1 = 0; let y1_1 = 0; let x1_2 = 0; let y1_2 = 0; let x2_1 = 0; let y2_1 = 0; let x2_2 = 0; let y2_2 = 0;
        if(startPoint.y === endPoint.y) {
            x1_1 = startPoint.x - CanvasSelectionManager.extensionLineWidth;
            y1_1 = startPoint.y - CanvasSelectionManager.extensionLineEndingSize;
            x1_2 = startPoint.x - CanvasSelectionManager.extensionLineWidth;
            y1_2 = startPoint.y + CanvasSelectionManager.extensionLineEndingSize;

            x2_1 = endPoint.x - CanvasSelectionManager.extensionLineWidth;
            y2_1 = startPoint.y - CanvasSelectionManager.extensionLineEndingSize;
            x2_2 = endPoint.x - CanvasSelectionManager.extensionLineWidth;
            y2_2 = startPoint.y + CanvasSelectionManager.extensionLineEndingSize;
        }
        else if(startPoint.x === endPoint.x) {
            x1_1 = startPoint.x - CanvasSelectionManager.extensionLineEndingSize;
            y1_1 = startPoint.y - CanvasSelectionManager.extensionLineWidth;
            x1_2 = startPoint.x + CanvasSelectionManager.extensionLineEndingSize;
            y1_2 = startPoint.y - CanvasSelectionManager.extensionLineWidth;

            x2_1 = startPoint.x - CanvasSelectionManager.extensionLineEndingSize;
            y2_1 = endPoint.y - CanvasSelectionManager.extensionLineWidth;
            x2_2 = startPoint.x + CanvasSelectionManager.extensionLineEndingSize;
            y2_2 = endPoint.y - CanvasSelectionManager.extensionLineWidth;
        }

        let sizeLineXCorr = 0; let sizeLineYCorr = 0;
        if(type === ExtensionLineType.RightToRightAbove || type === ExtensionLineType.RightToRightBelow)
            sizeLineXCorr -= CanvasSelectionManager.extensionLineWidth;
        if(type === ExtensionLineType.BottomToBottomAfter || type === ExtensionLineType.BottomToBottomBefore)
            sizeLineYCorr -= CanvasSelectionManager.extensionLineWidth;

        const children = [
            new PathPrimitive([
                PathPrimitiveMoveToCommand.fromPoint(startPoint.clone().offset(sizeLineXCorr, sizeLineYCorr)),
                PathPrimitiveLineToCommand.fromPoint(endPoint.clone().offset(sizeLineXCorr, sizeLineYCorr))
            ], StrokeStyle.default1pxInstance, "size-line"),
            new PathPrimitive([
                new PathPrimitiveMoveToCommand(x1_1, y1_1),
                new PathPrimitiveLineToCommand(x1_2, y1_2),
                new PathPrimitiveMoveToCommand(x2_1, y2_1),
                new PathPrimitiveLineToCommand(x2_2, y2_2)
            ], StrokeStyle.default1pxInstance),
            new TextPrimitive(
                (endPoint.x + startPoint.x) / 2,
                (endPoint.y + startPoint.y) / 2,
                text, TextOwner.ExtensionLine, undefined, undefined, undefined, null, undefined, null,
                PAGE_BG_TEXTFLOOR_FILTER_IDPREFIX + this.instanceId
            )
        ];

        const primitive = new GroupPrimitive(children, className);
        const ellEl = this.getExtensionLineElement(primitive, index);
        this.changePrimitiveElement(primitive, ellEl);
    }
    private hideExtensionLines() {
        for(let i = 0; i < this.extensionLineElements.length; i++)
            if(this.extensionLineElements[i])
                this.dom.changeByFunc(this.extensionLineElements[i], e => e.style.display = "none");

    }
    private getExtensionLineElement(primitive: SvgPrimitive<SVGElement>, index: number) {
        let ellEl = this.extensionLineElements[index];
        if(ellEl !== undefined)
            this.dom.changeByFunc(ellEl, e => e.style.display = "");
        else {
            ellEl = this.createPrimitiveElement(primitive, this.visualizersContainer);
            this.extensionLineElements[index] = (ellEl);
        }
        return ellEl;
    }

    protected getOrCreateShapeSelection(shape: Shape, usedItems?: ItemsMap): ShapeSelectionElement {
        let element = <ShapeSelectionElement> this.selectionMap[shape.key];
        if(!element) {
            element = new ShapeSelectionElement(this.itemSelectionContainer, this.selectionMarksContainer,
                this.actualZoom, this.readOnly, this.dom, shape.key, shape.isLocked, shape.rectangle, shape.style,
                shape.allowResizeHorizontally, shape.allowResizeVertically, shape.description.getParameterPoints(shape));
            this.selectionMap[shape.key] = element;
        }
        usedItems && (usedItems[shape.key] = true);
        return element;
    }
    protected getOrCreateConnectorSelection(connector: Connector, usedItems?: ItemsMap): ConnectorSelectionElement {
        let element = <ConnectorSelectionElement> this.selectionMap[connector.key];
        const points = connector.getRenderPoints(true);
        const pointsNonSkipped = connector.getRenderPoints(false);
        if(!element) {
            element = new ConnectorSelectionElement(this.itemSelectionContainer, this.selectionMarksContainer, this.actualZoom,
                this.readOnly, this.dom, connector.key, connector.isLocked, connector.rectangle, points,
                connector.style, connector.styleText, connector.enableText,
                connector.texts.map(t => {
                    const textInfo = GeometryUtils.getPathPointByPosition(pointsNonSkipped, t.position);
                    return {
                        text: connector.getText(t.position),
                        point: textInfo[0],
                        pointIndex: textInfo[1],
                        pos: t.position
                    };
                }).sort((a, b) => a.pos - b.pos), connector.points, connector.properties.lineOption);
            this.selectionMap[connector.key] = element;
        }
        usedItems && (usedItems[connector.key] = true);
        return element;
    }
    protected getOrCreateMultipleSelection(usedItems: ItemsMap) {
        let element = <MultipleSelectionElement> this.selectionMap[MULTIPLE_SELECTION_KEY];
        if(!element) {
            element = new MultipleSelectionElement(this.itemSelectionContainer, this.selectionMarksContainer, this.actualZoom, this.readOnly, this.dom);
            this.selectionMap[MULTIPLE_SELECTION_KEY] = element;
        }
        usedItems[MULTIPLE_SELECTION_KEY] = true;
        return element;
    }
    protected getMultipleSelection(): MultipleSelectionElement {
        return <MultipleSelectionElement> this.selectionMap[MULTIPLE_SELECTION_KEY];
    }
    protected updateShapeSelection(shape: Shape, multipleSelection: MultipleSelectionElement) {
        if(shape.key in this.selectionMap) {
            this.getOrCreateShapeSelection(shape).onModelChanged(shape.isLocked, shape.rectangle, shape.style,
                shape.allowResizeHorizontally, shape.allowResizeVertically, shape.description.getParameterPoints(shape));
            multipleSelection && multipleSelection.onModelItemChanged(shape.key, shape.rectangle);
        }
    }
    protected updateConnectorSelection(connector: Connector, multipleSelection: MultipleSelectionElement) {
        if(connector.key in this.selectionMap) {
            const renderPoints = connector.getRenderPoints(true);
            const renderPointsNonSkipped = connector.getRenderPoints(false);
            this.getOrCreateConnectorSelection(connector)
                .onModelChanged(connector.isLocked, connector.rectangle, renderPoints,
                    connector.style, connector.styleText, connector.enableText,
                    connector.texts.map(t => {
                        const textPos = GeometryUtils.getPathPointByPosition(renderPointsNonSkipped, t.position);
                        return {
                            text: connector.getText(t.position),
                            pointIndex: textPos[1],
                            pos: t.position,
                            point: textPos[0]
                        };
                    }).sort((a, b) => a.pos - b.pos), connector.points, connector.properties.lineOption);
            multipleSelection && multipleSelection.onModelItemChanged(connector.key, connector.rectangle);
        }
    }
    private hideOutdatedSelection(updated: ItemsMap) {
        Object.keys(this.selectionMap)
            .filter(k => !updated[k])
            .forEach(k => {
                this.selectionMap[k].destroy();
                delete this.selectionMap[k];
            });
    }
    private selectionCanBeDrawn(item: DiagramItem) {
        return !item.container || (item.container.expanded && this.selectionCanBeDrawn(item.container));
    }
    notifySelectionChanged(selection: Selection) {
        const items = selection.getSelectedItems(true).filter(item => this.selectionCanBeDrawn(item));
        const changedItems: ItemsMap = {};
        const isMultipleSelection = items.length > 1;
        const shapes = selection.getSelectedShapes(true).filter(shape => this.selectionCanBeDrawn(shape));
        const connectors = selection.getSelectedConnectors(true).filter(connector => this.selectionCanBeDrawn(connector));
        shapes.forEach(shape => this.getOrCreateShapeSelection(shape, changedItems).onSelectionChanged(isMultipleSelection));
        connectors.forEach(connector => this.getOrCreateConnectorSelection(connector, changedItems).onSelectionChanged(isMultipleSelection));
        if(isMultipleSelection) {
            const strokeWidth = items.length > 0 ? items[0].strokeWidth : 0;
            const rectangles = {};
            items.filter(i => !i.isLocked).forEach(item => rectangles[item.key] = item.rectangle);
            this.getOrCreateMultipleSelection(changedItems).onSelectionChanged(!!shapes.filter(i => !i.isLocked).length, strokeWidth, rectangles);
        }
        this.hideOutdatedSelection(changedItems);
    }
    applyChangesCore(changes: ItemChange[]) {
        super.applyChangesCore(changes);
        const multipleSelection = this.getMultipleSelection();
        multipleSelection && multipleSelection.onModelChanged();
    }
    applyChange(change: ItemChange) {
        const multipleSelection = this.getMultipleSelection();
        if(change.item instanceof Shape)
            this.updateShapeSelection(change.item, multipleSelection);
        else if(change.item instanceof Connector)
            this.updateConnectorSelection(change.item, multipleSelection);
    }
    notifyPageColorChanged(color: number) { }
    notifyPageSizeChanged(pageSize: Size, pageLandscape: boolean) { }
    notifyActualZoomChanged(actualZoom: number) {
        Object.keys(this.selectionMap).forEach(k => this.selectionMap[k].notifyZoomChanged(actualZoom));
        this.actualZoom = actualZoom;
    }
    notifyViewAdjusted(canvasOffset: Point) { }
    notifyReadOnlyChanged(readOnly: boolean) {
        this.readOnly = readOnly;
        Object.keys(this.selectionMap).forEach(k => this.selectionMap[k].notifyReadOnlyChanged(readOnly));
    }
    notifySelectionRectShow(rect: Rectangle) {
        this.showSelectionRect(rect.clone().multiply(this.actualZoom, this.actualZoom));
    }
    notifySelectionRectHide() {
        this.hideSelectionRect();
    }
    notifyResizeInfoShow(point: Point, text: string) {
        this.showResizeInfo(point.clone().multiply(this.actualZoom, this.actualZoom), text);
    }
    notifyResizeInfoHide() {
        this.hideResizeInfo();
    }
    notifyConnectionPointsShow(key: ItemKey, points: ConnectionPointInfo[], activePointIndex: number, outsideRectangle: Rectangle) {
        this.hideConnectionPoints();

        points.forEach((p, index) => {
            const point = p.point.clone().multiply(this.actualZoom, this.actualZoom);
            if(outsideRectangle)
                switch(p.side) {
                    case ConnectionPointSide.North:
                        point.y = outsideRectangle.y * this.actualZoom - CanvasSelectionManager.connectionPointShift;
                        break;
                    case ConnectionPointSide.South:
                        point.y = outsideRectangle.bottom * this.actualZoom + CanvasSelectionManager.connectionPointShift;
                        break;
                    case ConnectionPointSide.West:
                        point.x = outsideRectangle.x * this.actualZoom - CanvasSelectionManager.connectionPointShift;
                        break;
                    case ConnectionPointSide.East:
                        point.x = outsideRectangle.right * this.actualZoom + CanvasSelectionManager.connectionPointShift;
                        break;
                }
            this.showConnectionPoint(index, point, key, index, index === activePointIndex, p.allowed);
        });
    }
    notifyConnectionPointsHide() {
        this.hideConnectionPoints();
    }
    notifyConnectionTargetShow(key: ItemKey, info: ConnectionTargetInfo) {
        if(!info.allowed) return;

        const rect = CanvasSelectionManager.correctSelectionRect(info.rect.clone().multiply(this.actualZoom, this.actualZoom), info.strokeWidth,
            CanvasSelectionManager.connectionTargetBorderWidth, this.actualZoom, 0);
        this.showConnectionTarget(0, rect);
    }
    notifyConnectionTargetHide() {
        this.hideConnectionTarget();
    }
    notifyContainerTargetShow(key: ItemKey, info: ContainerTargetInfo) {
        const rect = CanvasSelectionManager.correctSelectionRect(info.rect.clone().multiply(this.actualZoom, this.actualZoom), info.strokeWidth,
            CanvasSelectionManager.connectionTargetBorderWidth, this.actualZoom, 0);
        this.showContainerTarget(0, rect);
    }
    notifyContainerTargetHide() {
        this.hideContainerTarget();
    }
    notifyExtensionLinesShow(lines: ExtensionLine[]) {
        this.hideExtensionLines();
        lines.forEach((line, index) => {
            this.showExtensionLine(index, line.type,
                line.segment.startPoint.clone().multiply(this.actualZoom, this.actualZoom),
                line.segment.endPoint.clone().multiply(this.actualZoom, this.actualZoom),
                line.text
            );
        });
    }
    notifyExtensionLinesHide() {
        this.hideExtensionLines();
    }
    notifyDragStart(itemKeys: ItemKey[]) {
        this.dom.changeByFunc(this.selectionMarksContainer, e => e.style.display = "none");
    }
    notifyDragEnd(itemKeys: ItemKey[]) {
        this.dom.changeByFunc(this.selectionMarksContainer, e => e.style.display = "");
    }
    notifyDragScrollStart() { }
    notifyDragScrollEnd() { }
    notifyTextInputStart(item: DiagramItem, text: string, position: Point, size?: Size): void {
        this.dom.changeByFunc(this.visualizersContainer, e => e.style.display = "none");
    }
    notifyTextInputEnd(item: DiagramItem, captureFocus?: boolean): void {
        this.dom.changeByFunc(this.visualizersContainer, e => e.style.display = "");
    }
    notifyTextInputPermissionsCheck(item: DiagramItem, allowed: boolean): void {}

    static correctSelectionRect(rect: Rectangle, targetLineWidth: number, selectionLineWidth: number,
        zoomLevel: number, outsideOffset: number = CanvasSelectionManager.selectionOffset): Rectangle {
        const evenOddWidth = UnitConverter.twipsToPixels(targetLineWidth) % 2 !== UnitConverter.twipsToPixels(selectionLineWidth) % 2;
        const corr = Math.ceil(targetLineWidth / 2 * zoomLevel);
        rect = rect.clone().inflate(corr, corr);
        const lwCorr = Math.floor(selectionLineWidth / 2);
        rect.x -= lwCorr;
        rect.y -= lwCorr;
        rect.width += selectionLineWidth;
        rect.height += selectionLineWidth;
        if(evenOddWidth) {
            const correction = CanvasSelectionManager.evenOddSelectionCorrection * (UnitConverter.twipsToPixels(selectionLineWidth) % 2 === 1 ? -1 : 1);
            rect = rect.clone().moveRectangle(correction, correction);
        }
        return rect.clone().inflate(outsideOffset, outsideOffset);
    }
}

abstract class CanvasElement implements IZoomChangesListener, IReadOnlyChangesListener {
    private elements: { [key: string]: SVGElement } = {};
    private updatedElements: { [key: string]: boolean } = {};
    constructor(
        protected rectsContainer: SVGElement,
        protected marksContainer: SVGElement,
        protected key: ItemKey,
        protected zoomLevel: number,
        protected readOnly: boolean,
        protected dom: DOMManipulator) { }
    notifyZoomChanged(zoom: number) {
        if(this.zoomLevel !== zoom) {
            this.zoomLevel = zoom;
            this.redraw();
        }
    }
    notifyReadOnlyChanged(readOnly: boolean) {
        this.readOnly = readOnly;
        this.redraw();
    }
    destroy() {
        Object.keys(this.elements)
            .forEach(key => {
                this.elements[key].parentNode.removeChild(this.elements[key]);
                delete this.elements[key];
            });
    }
    protected redraw() {
        this.updatedElements = {};
        this.redrawCore();
        Object.keys(this.elements)
            .filter(key => !this.updatedElements[key])
            .forEach(key => {
                this.elements[key].parentNode.removeChild(this.elements[key]);
                delete this.elements[key];
            });
        this.updatedElements = {};
    }
    protected abstract redrawCore();
    protected drawSelectionMarks(rect: Rectangle, allowResizeHorizontally: boolean, allowResizeVertically: boolean) {
        if(this.readOnly) return;
        const hasEWMarks = allowResizeHorizontally && rect.height > CanvasSelectionManager.selectionMarkSize * 3;
        const hasNSMarks = allowResizeVertically && rect.width > CanvasSelectionManager.selectionMarkSize * 3;
        const hasCornerMarks = allowResizeHorizontally || allowResizeVertically;
        if(hasCornerMarks)
            this.drawSelectionMark(0, new Point(rect.x, rect.y), CanvasSelectionManager.selectionMarkSize,
                MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_NW, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK);
        if(hasNSMarks && !Browser.TouchUI)
            this.drawSelectionMark(1, new Point(rect.x + rect.width / 2, rect.y), CanvasSelectionManager.selectionMarkSize,
                MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_N, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK);
        if(hasCornerMarks)
            this.drawSelectionMark(2, new Point(rect.right, rect.y), CanvasSelectionManager.selectionMarkSize,
                MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_NE, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK);
        if(hasEWMarks && !Browser.TouchUI)
            this.drawSelectionMark(3, new Point(rect.right, rect.y + rect.height / 2), CanvasSelectionManager.selectionMarkSize,
                MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_E, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK);
        if(hasCornerMarks)
            this.drawSelectionMark(4, new Point(rect.right, rect.bottom), CanvasSelectionManager.selectionMarkSize,
                MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_SE, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK);
        if(hasNSMarks && !Browser.TouchUI)
            this.drawSelectionMark(5, new Point(rect.x + rect.width / 2, rect.bottom), CanvasSelectionManager.selectionMarkSize,
                MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_S, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK);
        if(hasCornerMarks)
            this.drawSelectionMark(6, new Point(rect.x, rect.bottom), CanvasSelectionManager.selectionMarkSize,
                MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_SW, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK);
        if(hasEWMarks && !Browser.TouchUI)
            this.drawSelectionMark(7, new Point(rect.x, rect.y + rect.height / 2), CanvasSelectionManager.selectionMarkSize,
                MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_W, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK);
    }
    protected drawSelectionMark(index: number, point: Point, size: number, type: MouseEventElementType, value: any, className: string) {
        this.getOrCreateElement("SM" + index, new RectanglePrimitive(point.x - size / 2, point.y - size / 2, size, size, null, className, undefined, el => {
            RenderUtils.setElementEventData(el, type, this.key, value);
        }), this.marksContainer);
    }
    protected drawSelectionRect(rectangle: Rectangle, type: MouseEventElementType, className: string) {
        const primitive = new RectanglePrimitive(rectangle.x, rectangle.y, rectangle.width, rectangle.height,
            StrokeStyle.default1pxInstance, className, undefined, el => {
                RenderUtils.setElementEventData(el, type, "-1", -1);
            }
        );
        this.getOrCreateElement("shapeSelection", primitive, this.rectsContainer);
    }
    protected getOrCreateElement(cacheKey: string, primitive: SvgPrimitive<SVGElement>, container: SVGElement) {
        let element = this.elements[cacheKey];
        if(!element) {
            element = primitive.createElement(el => container.appendChild(el));
            this.elements[cacheKey] = element;
        }
        this.updatedElements[cacheKey] = true;
        this.dom.changeByPrimitive(element, primitive);
        return element;
    }
}

abstract class ItemSelectionElement extends CanvasElement {
    protected isMultipleSelection: boolean;

    constructor(rectsContainer: SVGElement, marksContainer: SVGElement, key: ItemKey,
        zoomLevel: number, readOnly: boolean, dom: DOMManipulator,
        protected isLocked: boolean,
        protected rectangle: Rectangle) {
        super(rectsContainer, marksContainer, key, zoomLevel, readOnly, dom);
    }

    onSelectionChanged(isMultipleSelection: boolean) {
        if(this.isMultipleSelection !== isMultipleSelection) {
            this.isMultipleSelection = isMultipleSelection;
            this.redraw();
        }
    }

    protected isLockedRender() {
        return this.isLocked && !this.readOnly;
    }
    protected drawLockedSelectionMark(index: number, point: Point, size: number, className: string) {
        const primitive = new PathPrimitive([
            new PathPrimitiveMoveToCommand(point.x - size / 2, point.y - size / 2),
            new PathPrimitiveLineToCommand(point.x + size / 2, point.y + size / 2),
            new PathPrimitiveMoveToCommand(point.x + size / 2, point.y - size / 2),
            new PathPrimitiveLineToCommand(point.x - size / 2, point.y + size / 2)
        ], null, className);
        this.getOrCreateElement("LSM" + index, primitive, this.marksContainer);
    }
}

class MultipleSelectionElement extends CanvasElement {
    private needDrawSelectionMarks: boolean;
    private strokeWidth: number;
    private rectangles: {[key: string]: Rectangle} = {};

    constructor(rectsContainer: SVGElement, marksContainer: SVGElement, zoomLevel: number, readOnly: boolean, dom: DOMManipulator) {
        super(rectsContainer, marksContainer, "-1", zoomLevel, readOnly, dom);
    }
    onModelItemChanged(key: ItemKey, rectangle: Rectangle) {
        if(key in this.rectangles)
            this.rectangles[key] = rectangle;
    }
    onModelChanged() {
        this.redraw();
    }
    onSelectionChanged(needDrawSelectionMarks: boolean, strokeWidth: number, rectangles: {[key: string]: Rectangle}) {
        this.needDrawSelectionMarks = needDrawSelectionMarks;
        this.strokeWidth = strokeWidth;
        this.rectangles = rectangles;
        this.redraw();
    }
    protected redrawCore() {
        const rectKeys = Object.keys(this.rectangles);
        if(!rectKeys.length) return;

        const rect = GeometryUtils.getCommonRectangle(rectKeys.map(key => this.rectangles[key])).clone().multiply(this.zoomLevel, this.zoomLevel);
        const selRect = CanvasSelectionManager.correctSelectionRect(rect,
            this.strokeWidth, CanvasSelectionManager.selectionRectLineWidth, this.zoomLevel);
        this.drawSelectionRect(selRect, MouseEventElementType.SelectionRect, SELECTION_ELEMENT_CLASSNAMES.ITEMS_SELECTION_RECT);
        if(this.needDrawSelectionMarks)
            this.drawSelectionMarks(rect, true, true);
    }
}

class ShapeSelectionElement extends ItemSelectionElement {
    constructor(rectsContainer: SVGElement, marksContainer: SVGElement, zoomLevel: number, readOnly: boolean,
        dom: DOMManipulator, key: ItemKey, isLocked: boolean, rectangle: Rectangle,
        protected style: Style,
        protected allowResizeHorizontally: boolean,
        protected allowResizeVertically: boolean,
        protected shapeParameterPoints: ShapeParameterPoint[]) {
        super(rectsContainer, marksContainer, key, zoomLevel, readOnly, dom, isLocked, rectangle);
    }

    onModelChanged(isLocked: boolean, rectangle: Rectangle, style: Style,
        allowResizeHorizontally: boolean, allowResizeVertically: boolean, shapeParameterPoints: ShapeParameterPoint[]) {
        this.isLocked = isLocked;
        this.rectangle = rectangle;
        this.style = style;
        this.allowResizeHorizontally = allowResizeHorizontally;
        this.allowResizeVertically = allowResizeVertically;
        this.shapeParameterPoints = shapeParameterPoints;
        this.redraw();
    }

    protected redrawCore() {
        const rect = this.rectangle.clone().multiply(this.zoomLevel, this.zoomLevel);
        if(this.isLockedRender())
            this.drawLockedSelection(rect);
        else
            this.drawUnlockedSelection(rect);
    }
    private drawLockedSelection(rect: Rectangle) {
        this.drawLockedSelectionMark(0, new Point(rect.x, rect.y),
            CanvasSelectionManager.lockedSelectionMarkSize, SELECTION_ELEMENT_CLASSNAMES.LOCKED_SELECTION_MARK);
        this.drawLockedSelectionMark(1, new Point(rect.right, rect.y),
            CanvasSelectionManager.lockedSelectionMarkSize, SELECTION_ELEMENT_CLASSNAMES.LOCKED_SELECTION_MARK);
        this.drawLockedSelectionMark(2, new Point(rect.right, rect.bottom),
            CanvasSelectionManager.lockedSelectionMarkSize, SELECTION_ELEMENT_CLASSNAMES.LOCKED_SELECTION_MARK);
        this.drawLockedSelectionMark(3, new Point(rect.x, rect.bottom),
            CanvasSelectionManager.lockedSelectionMarkSize, SELECTION_ELEMENT_CLASSNAMES.LOCKED_SELECTION_MARK);
    }
    private drawUnlockedSelection(rect: Rectangle) {
        const selRect = CanvasSelectionManager.correctSelectionRect(rect, this.style.strokeWidth,
            CanvasSelectionManager.selectionRectLineWidth, this.zoomLevel);
        this.drawSelectionRect(selRect, MouseEventElementType.SelectionRect, this.isMultipleSelection ? SELECTION_ELEMENT_CLASSNAMES.ITEM_MULTI_SELECTION : SELECTION_ELEMENT_CLASSNAMES.ITEM_SELECTION_RECT);
        if(!this.isMultipleSelection)
            this.drawSelectionMarks(rect, this.allowResizeHorizontally, this.allowResizeVertically);
        this.drawShapeParameterPoints();
    }

    private drawShapeParameterPoints() {
        if(this.readOnly) return;
        this.shapeParameterPoints.forEach((pp, index) => {
            const point = pp.point.clone().multiply(this.zoomLevel, this.zoomLevel);
            this.drawShapeParameterPoint(point, index, pp.key);
        });
    }
    private drawShapeParameterPoint(point: Point, index: number, pointKey: string) {
        const size = CanvasSelectionManager.geomertyMarkSize;
        const primitive = new RectanglePrimitive(point.x - size / 2, point.y - size / 2,
            size, size, null, "geometry-mark", undefined, el => {
                RenderUtils.setElementEventData(el, MouseEventElementType.ShapeParameterBox, this.key, pointKey);
            });
        this.getOrCreateElement("pp" + index.toString(), primitive, this.marksContainer);
    }
}

class ConnectorSelectionElement extends ItemSelectionElement {
    constructor(rectsContainer: SVGElement, marksContainer: SVGElement, zoomLevel: number, readOnly: boolean,
        dom: DOMManipulator, key: string, isLocked: boolean, rectangle: Rectangle,
        protected renderPoints: ConnectorRenderPoint[],
        protected style: Style,
        protected styleText: TextStyle,
        protected enableText: boolean,
        protected texts: {text: string, point: Point, pointIndex: number}[],
        protected points: Point[],
        protected lineType: ConnectorLineOption) {
        super(rectsContainer, marksContainer, key, zoomLevel, readOnly, dom, isLocked, rectangle);
    }
    onModelChanged(isLocked: boolean, rectangle: Rectangle,
        renderPoints: ConnectorRenderPoint[],
        style: Style, styleText: TextStyle, enableText: boolean,
        texts: {text: string, point: Point, pointIndex: number}[], points: Point[], lineType: ConnectorLineOption) {
        this.isLocked = isLocked;
        this.rectangle = rectangle;
        this.renderPoints = renderPoints;
        this.style = style;
        this.styleText = styleText;
        this.enableText = enableText;
        this.texts = texts;
        this.points = points;
        this.lineType = lineType;
        this.redraw();
    }
    protected redrawCore() {
        if(this.isLockedRender())
            this.drawLockedSelection();
        else
            this.drawUnlockedSelection();
    }
    protected drawLockedSelection() {
        this.renderPoints.forEach((pt, index) => {
            this.drawLockedSelectionMark(index, pt,
                CanvasSelectionManager.lockedSelectionMarkSize, SELECTION_ELEMENT_CLASSNAMES.LOCKED_SELECTION_MARK);
        });
    }
    protected drawUnlockedSelection() {
        this.drawConnectorSelection();
        if(!this.isMultipleSelection && !this.readOnly)
            this.drawConnectorSelectionMarks();
    }
    private drawConnectorSelection() {
        const commands: PathPrimitiveCommand[] = [];
        const commandsWB: PathPrimitiveCommand[] = [];
        const className = this.isMultipleSelection ? SELECTION_ELEMENT_CLASSNAMES.CONNECTOR_MULTI_SELECTION : SELECTION_ELEMENT_CLASSNAMES.CONNECTOR_SELECTION;
        this.populateSelectionPrimitiveCommands(commands, commandsWB);
        const primitive = new PathPrimitive(commands.concat(commandsWB.reverse()), StrokeStyle.default1pxInstance, className);
        this.getOrCreateElement("CS", primitive, this.rectsContainer);
    }
    populateSelectionPrimitiveCommands(commands: PathPrimitiveCommand[], commandsWB: PathPrimitiveCommand[]) {
        const texts = this.texts;
        const txtAlign = this.styleText.getAlignment();
        const points = this.createNotSkippedRenderPoints();
        const zoomLevel = this.zoomLevel;
        const strokeWidthPx = this.style.strokeWidthPx;
        const selectionOffset = this.getSelectionOffset(strokeWidthPx);
        const strokeWidthPxIsEvenOdd = strokeWidthPx % 2 === 0;

        let prevPt: Point = points[0];
        let textIndex = 0;

        let offset: Point;
        let distance: number;

        let nextOffset: Point;
        let nextDistance: number;

        for(let i = 1, pt: ConnectorRenderPoint; pt = points[i]; i++) {
            const nextPt: Point = points[i + 1];
            if(offset === undefined) {
                distance = Metrics.euclideanDistance(prevPt, pt);
                if(MathUtils.numberCloseTo(distance, 0))
                    continue;
                offset = GeometryUtils.getSelectionOffsetPoint(prevPt, pt, distance).multiply(selectionOffset, selectionOffset);
            }
            if(nextPt) {
                nextDistance = Metrics.euclideanDistance(pt, nextPt);
                if(MathUtils.numberCloseTo(nextDistance, 0))
                    continue;
                nextOffset = GeometryUtils.getSelectionOffsetPoint(pt, nextPt, nextDistance).multiply(selectionOffset, selectionOffset);
            }
            let offsetX = offset.x;
            let offsetY = offset.y;
            let offsetXNegative = -offsetX;
            let offsetYNegative = -offsetY;

            let nextOffsetX = nextOffset && nextOffset.x;
            let nextOffsetY = nextOffset && nextOffset.y;
            let nextOffsetXNegative = nextOffset && -nextOffset.x;
            let nextOffsetYNegative = nextOffset && -nextOffset.y;

            if(strokeWidthPxIsEvenOdd) {
                if(offsetXNegative > 0)
                    offsetXNegative -= CanvasSelectionManager.evenOddSelectionCorrection;
                else if(offsetX > 0)
                    offsetX -= CanvasSelectionManager.evenOddSelectionCorrection;
                if(offsetYNegative > 0)
                    offsetYNegative -= CanvasSelectionManager.evenOddSelectionCorrection;
                else if(offsetY > 0)
                    offsetY -= CanvasSelectionManager.evenOddSelectionCorrection;

                if(nextOffsetXNegative > 0)
                    nextOffsetXNegative -= CanvasSelectionManager.evenOddSelectionCorrection;
                else if(nextOffsetX > 0)
                    nextOffsetX -= CanvasSelectionManager.evenOddSelectionCorrection;
                if(nextOffsetYNegative > 0)
                    nextOffsetYNegative -= CanvasSelectionManager.evenOddSelectionCorrection;
                else if(nextOffsetY > 0)
                    nextOffsetY -= CanvasSelectionManager.evenOddSelectionCorrection;
            }

            while(texts[textIndex] && texts[textIndex].pointIndex <= i) {
                const text = texts[textIndex];
                const size = this.getConnectorSelectionTextSize(text.text, selectionOffset);

                const textPts = GeometryUtils.getSelectionTextStartEndPoints(prevPt, pt, distance, text.point, size, txtAlign);
                if(texts[textIndex].pointIndex < i) {
                    prevPt = textPts[1];
                    commands.push(PathPrimitiveMoveToCommand.fromPoint(prevPt.clone().offset(offsetX, offsetY).multiply(zoomLevel, zoomLevel)));
                    commandsWB.push(PathPrimitiveLineToCommand.fromPoint(prevPt.clone().offset(offsetXNegative, offsetYNegative).multiply(zoomLevel, zoomLevel)));
                }
                else {
                    if(!commands.length) {
                        commands.push(PathPrimitiveMoveToCommand.fromPoint(prevPt.clone().offset(offsetX, offsetY).multiply(zoomLevel, zoomLevel)));
                        commandsWB.push(PathPrimitiveLineToCommand.fromPoint(prevPt.clone().offset(offsetXNegative, offsetYNegative).multiply(zoomLevel, zoomLevel)));
                    }
                    commands.push(PathPrimitiveLineToCommand.fromPoint(textPts[0].clone().offset(offsetX, offsetY).multiply(zoomLevel, zoomLevel)));
                    commands.push(PathPrimitiveMoveToCommand.fromPoint(textPts[1].clone().offset(offsetX, offsetY).multiply(zoomLevel, zoomLevel)));
                    commandsWB.push(PathPrimitiveMoveToCommand.fromPoint(textPts[0].clone().offset(offsetXNegative, offsetYNegative).multiply(zoomLevel, zoomLevel)));
                    commandsWB.push(PathPrimitiveLineToCommand.fromPoint(textPts[1].clone().offset(offsetXNegative, offsetYNegative).multiply(zoomLevel, zoomLevel)));
                    prevPt = textPts[1];
                }
                textIndex++;
            }

            if(!commands.length) {
                commands.push(PathPrimitiveMoveToCommand.fromPoint(prevPt.clone().offset(offsetX, offsetY).multiply(zoomLevel, zoomLevel)));
                commandsWB.push(PathPrimitiveLineToCommand.fromPoint(prevPt.clone().offset(offsetXNegative, offsetYNegative).multiply(zoomLevel, zoomLevel)));
            }
            if(nextPt) {
                GeometryUtils.addSelectedLinesTo(
                    prevPt, pt, nextPt,
                    offsetX, offsetY,
                    offsetXNegative, offsetYNegative,
                    nextOffsetX, nextOffsetY,
                    nextOffsetXNegative, nextOffsetYNegative,
                    (x, y) => commands.push(new PathPrimitiveLineToCommand(x * zoomLevel, y * zoomLevel)),
                    (x, y) => commandsWB.push(new PathPrimitiveLineToCommand(x * zoomLevel, y * zoomLevel)));
                offset = nextOffset;
                distance = nextDistance;
            }
            else {
                commands.push(PathPrimitiveLineToCommand.fromPoint(pt.clone().offset(offsetX, offsetY).multiply(zoomLevel, zoomLevel)));
                commandsWB.push(PathPrimitiveMoveToCommand.fromPoint(pt.clone().offset(offsetXNegative, offsetYNegative).multiply(zoomLevel, zoomLevel)));
            }
            prevPt = pt;
        }
    }

    private createNotSkippedRenderPoints() : ConnectorRenderPoint[] {
        const clonedRenderPoints = this.renderPoints.map(p => p.clone());
        if(this.lineType === ConnectorLineOption.Straight)
            ModelUtils.skipUnnecessaryRenderPoints(clonedRenderPoints);
        else
            ModelUtils.skipUnnecessaryRightAngleRenderPoints(clonedRenderPoints);
        return clonedRenderPoints.filter(p => !p.skipped);
    }
    private getSelectionOffset(strokeWidthPx: number): number {
        return CanvasSelectionManager.selectionOffset + UnitConverter.pixelsToTwips(Math.round(strokeWidthPx / 2) + (strokeWidthPx + 1) % 2);
    }
    private getConnectorSelectionTextSize(text: string, selectionOffset : number) : Size {
        return this.dom.measurer.measureTextLine(text, this.styleText, TextOwner.Connector)
            .applyConverter(UnitConverter.pixelsToTwips)
            .clone().offset(selectionOffset, selectionOffset).nonNegativeSize();
    }

    drawConnectorSelectionMarks() {
        const pointsCount = this.points.length - 1;
        this.points.forEach((pt, index) => {
            const isEdgePoint = index === 0 || index === pointsCount;
            const className = isEdgePoint ? SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK : SELECTION_ELEMENT_CLASSNAMES.CONNECTOR_POINT_MARK;
            const markSize = isEdgePoint ? CanvasSelectionManager.selectionMarkSize : CanvasSelectionManager.connectorPointMarkSize;
            if(isEdgePoint || this.lineType === ConnectorLineOption.Straight)
                this.drawSelectionMark(index, pt.clone().multiply(this.zoomLevel, this.zoomLevel), markSize,
                    MouseEventElementType.ConnectorPoint, index, className);

            else
                this.drawSelectionMark(index, pt.clone().multiply(this.zoomLevel, this.zoomLevel), markSize,
                    MouseEventElementType.Undefined, -1, className + " disabled");

        });
        this.drawConnectorSideMarks();
    }
    drawConnectorSideMarks() {
        const type = (this.lineType === ConnectorLineOption.Straight) ?
            MouseEventElementType.ConnectorSide : MouseEventElementType.ConnectorOrthogonalSide;
        let prevPt: ConnectorRenderPoint; let prevPtIndex: number;
        this.renderPoints.forEach((pt, index) => {
            if(pt.skipped) return;
            if(prevPt !== undefined)
                if(this.canDrawConnectorSideMark(pt, prevPt)) {
                    const classNameSuffix = this.lineType === ConnectorLineOption.Orthogonal ?
                        (pt.x - prevPt.x === 0 ? "vertical" : "horizontal") : "";
                    this.drawSelectionMark(this.points.length + index - 1,
                        new Point(
                            prevPt.x + (pt.x - prevPt.x) / 2,
                            prevPt.y + (pt.y - prevPt.y) / 2
                        ).clone().multiply(this.zoomLevel, this.zoomLevel),
                        CanvasSelectionManager.connectorSideMarkSize,
                        type, prevPtIndex + "_" + index, SELECTION_ELEMENT_CLASSNAMES.CONNECTOR_SIDE_MARK + " " + classNameSuffix);
                }

            prevPt = pt;
            prevPtIndex = index;
        });
    }
    canDrawConnectorSideMark(current: ConnectorRenderPoint, prev: ConnectorRenderPoint) {
        if(this.lineType === ConnectorLineOption.Straight) {
            const minSize = CanvasSelectionManager.selectionMarkSize + CanvasSelectionManager.connectorSideMarkSize;
            return Metrics.euclideanDistance(current, prev) > minSize;
        }
        if(this.lineType === ConnectorLineOption.Orthogonal) {
            const hasBeginPoint = prev.pointIndex === 0;
            const hasEndPoint = GeometryUtils.areDuplicatedPoints(this.points[this.points.length - 1], current);
            if(hasBeginPoint && hasEndPoint)
                return Metrics.euclideanDistance(current, prev) > 2 * Connector.minOffset;
            if(!hasBeginPoint && hasEndPoint || hasBeginPoint && !hasEndPoint)
                return Metrics.euclideanDistance(current, prev) > Connector.minOffset;
            const minSize = CanvasSelectionManager.selectionMarkSize + CanvasSelectionManager.connectorSideMarkSize;
            return Metrics.euclideanDistance(current, prev) > minSize;
        }
        return false;
    }
}
