import { ShapeTypes } from "../Model/Shapes/ShapeTypes";
import { DiagramItem, ItemDataKey } from "../Model/DiagramItem";
import { Shape } from "../Model/Shapes/Shape";
import { Connector } from "../Model/Connectors/Connector";
import { DiagramModel } from "../Model/Model";
import { INodeDataImporter, IEdgeDataImporter, IDataChangesListener, IItemDataImporter, IDataImportParameters } from "./Interfaces";
import { DataSource } from "./DataSource";
import { DataSourceNodeItem, DataSourceEdgeItem, DataSourceItem } from "./DataSourceItems";
import { ModelUtils } from "../Model/ModelUtils";
import { Data } from "../Utils/Data";
import { ObjectUtils } from "../Utils";
import { DiagramUnit } from "../Enums";
import { StringUtils } from "@devexpress/utils/lib/utils/string";
import { MathUtils } from "@devexpress/utils/lib/utils/math";

export class UpdateNodeKeyRelatedObjectsStackItem {
    constructor(public shape: Shape, public nodeObj: DataSourceNodeItem) {
    }
}

export class UpdateNodeKeyRelatedObjectsStackAction {
    constructor(public kind: string, public nodeObj: any) {
    }
}

type DelayedNodeNotifications = {
    inserts: [Shape, DataSourceNodeItem][],
    updates: [Shape, DataSourceNodeItem][],
    deletes: [ItemDataKey, DataSourceItem][]
};

export class DocumentDataSource extends DataSource {
    private nodeInsertingLockCount: number = 0;
    private updateNodeKeyRelatedObjectsCount: number = 0;
    updateNodeKeyRelatedObjectsStack: UpdateNodeKeyRelatedObjectsStackItem[] = [];
    updateNodeKeyRelatedObjectsStackActions: UpdateNodeKeyRelatedObjectsStackAction[] = [];

    constructor(private changesListener: IDataChangesListener,
        nodeDataSource: any[],
        edgeDataSource: any[],
        parameters?: IDataImportParameters,
        nodeDataImporter?: INodeDataImporter,
        edgeDataImporter?: IEdgeDataImporter) {
        super("Document", nodeDataSource, edgeDataSource, parameters, nodeDataImporter, edgeDataImporter);
    }

    updateItemsByModel(model: DiagramModel): void {
        this.beginChangesNotification();

        const nodeNotifications: DelayedNodeNotifications = {
            inserts: [],
            updates: [],
            deletes: []
        };

        this.deleteNodes(model, nodeNotifications);
        model.items.forEach(item => {
            if(item instanceof Shape)
                this.updateNode(model, item, nodeNotifications);
        });
        model.items.forEach(item => {
            if(item instanceof Shape)
                this.updateNodeObjectConnectedProperties(item, this.findNode(item.dataKey), this.changesListener);
        });
        this.applyDelayedNodeNotifications(nodeNotifications);

        this.deleteEdges(model);
        model.items.forEach(item => {
            if(item instanceof Connector)
                this.updateEdge(model, item);
        });
        this.endChangesNotification(false);
    }

    protected applyDelayedNodeNotifications(notification: DelayedNodeNotifications): void {
        notification.deletes.forEach(element => {
            this.beginChangesNotification();
            this.changesListener.notifyNodeRemoved.call(this.changesListener, element[0], element[1].dataObj,
                (_key: ItemDataKey, _data: any) => {
                    this.endChangesNotification(false);
                },
                (_error: any) => {
                    this.endChangesNotification(false);
                }
            );
        });
        notification.inserts.forEach(element => {
            this.beginChangesNotification();
            this.beginNodeInserting();
            this.changesListener.notifyNodeInserted.call(this.changesListener, element[1].dataObj,
                (data: any) => {
                    this.updateNodeObjectKey(element[0], element[1], data);
                    this.endNodeInserting();
                    this.endChangesNotification(false);
                },
                (_: any) => {
                    this.endNodeInserting();
                    this.endChangesNotification(false);
                }
            );
        });
        notification.updates.forEach(element => {
            this.beginChangesNotification();
            this.changesListener.notifyNodeUpdated.call(this.changesListener,
                this.nodeDataImporter.getKey(element[1].dataObj || element[1].key), element[1].dataObj,
                (_key: ItemDataKey, _data: any) => {
                    this.endChangesNotification(false);
                },
                (_error: any) => {
                    this.endChangesNotification(false);
                }
            );
        });
    }

    protected isItemObjectModified(item: DiagramItem, itemObj: DataSourceItem, importer: IItemDataImporter): boolean {
        let modified = (importer.setLocked && itemObj.locked !== item.locked) ||
            (importer.setZIndex && itemObj.zIndex !== item.zIndex) ||
            (importer.setCustomData && !ObjectUtils.compareObjects(itemObj.customData, item.customData));
        if(!modified && importer.setStyle) {
            const defaultStyle = item.style.getDefaultInstance();
            item.style.forEach(key => {
                if(item.style[key] !== defaultStyle[key] && item.style[key] !== (itemObj.style && itemObj.style[key]))
                    modified = true;
            });
        }
        if(!modified && importer.setStyleText) {
            const defaultTextStyle = item.styleText.getDefaultInstance();
            item.styleText.forEach(key => {
                if(item.styleText[key] !== defaultTextStyle[key] && item.styleText[key] !== (itemObj.styleText && itemObj.styleText[key]))
                    modified = true;
            });
        }
        return modified;
    }
    protected setDataObjectKeyRelatedProperty(method: (dataObj: any, key: ItemDataKey) => void, dataObj: any, key: ItemDataKey, allowAutoGeneratedProperty: boolean): void {
        if(allowAutoGeneratedProperty || this.autoGeneratedDataKeys[key] === undefined)
            method(dataObj, key);
    }
    protected updateItemObjectProperties(itemObj: DataSourceItem, item: DiagramItem, importer: IItemDataImporter): void {
        if(importer.setCustomData) {
            itemObj.customData = ObjectUtils.cloneObject(item.customData);
            if(itemObj.dataObj && itemObj.customData !== undefined)
                importer.setCustomData(itemObj.dataObj, item.customData);
        }
        if(importer.setLocked) {
            itemObj.locked = item.locked;
            if(itemObj.dataObj && itemObj.locked !== undefined)
                importer.setLocked(itemObj.dataObj, item.locked);
        }
        if(importer.setStyle) {
            const styleObj = item.style.toObject();
            itemObj.style = styleObj;
            if(itemObj.dataObj && itemObj.style !== undefined)
                importer.setStyle(itemObj.dataObj, Data.objectToCssText(styleObj));
        }
        if(importer.setStyleText) {
            const styleTextObj = item.styleText.toObject();
            itemObj.styleText = styleTextObj;
            if(itemObj.dataObj && itemObj.styleText !== undefined)
                importer.setStyleText(itemObj.dataObj, Data.objectToCssText(styleTextObj));
        }
        if(importer.setZIndex) {
            itemObj.zIndex = item.zIndex;
            if(itemObj.dataObj && itemObj.zIndex !== undefined)
                importer.setZIndex(itemObj.dataObj, item.zIndex);
        }
    }
    protected deleteItems(dataSourceItems: DataSourceItem[],
        findItem: (ItemKey) => any,
        getParentArray: (DataSourceItem) => any[],
        callback: (DataSourceItem, boolean) => void): void {
        const items = dataSourceItems.slice();
        items.forEach(item => {
            if(item.key !== undefined && item.key !== null && !findItem(item.key)) {
                const parentArray = getParentArray(item);
                const index = parentArray.indexOf(item.dataObj);
                parentArray.splice(index, 1);
                callback(item, index > -1);
            }
        });
    }
    protected updateNode(model: DiagramModel, shape: Shape, notifications: DelayedNodeNotifications): void {
        let nodeObj = this.findNode(shape.dataKey);
        if(!nodeObj) {
            const dataObj = {};
            if(shape.dataKey !== undefined && shape.dataKey !== null)
                this.nodeDataImporter.setKey(dataObj, shape.dataKey);
            nodeObj = this.addNodeInternal(dataObj, shape.description.key, shape.text);
            this.nodeDataSource.push(nodeObj.dataObj);
            this.setDataObjectKeyRelatedProperty(this.nodeDataImporter.setKey, dataObj, nodeObj.key, this.addInternalKeyOnInsert);
            this.updateNodeObjectProperties(shape, nodeObj, model.units);
            this.updateNodeObjectConnectedProperties(shape, nodeObj);
            this.updateNodeObjectKey(shape, nodeObj, nodeObj.dataObj);
            notifications.inserts.push([shape, nodeObj]);
        }
        else if(this.isNodeObjectModified(shape, nodeObj, model.units)) {
            this.updateNodeObjectProperties(shape, nodeObj, model.units);
            this.updateNodeObjectConnectedProperties(shape, nodeObj);
            notifications.updates.push([shape, nodeObj]);
        }
        else this.updateNodeObjectConnectedProperties(shape, nodeObj, this.changesListener);

    }
    areImageUrlsEqual(url1: string, url2: string): boolean {
        return (url1 === url2) ||
            (StringUtils.isNullOrEmpty(url1) && StringUtils.isNullOrEmpty(url2));
    }
    protected isNodeObjectModified(shape: Shape, nodeObj: DataSourceNodeItem, units: DiagramUnit): boolean {
        return this.isItemObjectModified(shape, nodeObj, this.nodeDataImporter) ||
            (nodeObj.type !== shape.description.key && !(nodeObj.type === undefined && shape.description.key === ShapeTypes.Rectangle)) ||
            !this.compareStrings(nodeObj.text, shape.text) ||
            (this.nodeDataImporter.setImage && !this.areImageUrlsEqual(nodeObj.image, shape.image.actualUrl)) ||
            (this.nodeDataImporter.setLeft && !MathUtils.numberCloseTo(nodeObj.left, ModelUtils.getlUnitValue(units, shape.position.x))) ||
            (this.nodeDataImporter.setTop && !MathUtils.numberCloseTo(nodeObj.top, ModelUtils.getlUnitValue(units, shape.position.y))) ||
            (this.nodeDataImporter.setWidth && !MathUtils.numberCloseTo(nodeObj.width, ModelUtils.getlUnitValue(units, shape.size.width))) ||
            (this.nodeDataImporter.setHeight && !MathUtils.numberCloseTo(nodeObj.height, ModelUtils.getlUnitValue(units, shape.size.height)));
    }
    protected updateNodeObjectProperties(shape: Shape, nodeObj: DataSourceNodeItem, units: DiagramUnit): void {
        this.updateItemObjectProperties(nodeObj, shape, this.nodeDataImporter);
        if(this.nodeDataImporter.setType) {
            nodeObj.type = shape.description.key;
            this.nodeDataImporter.setType(nodeObj.dataObj, shape.description.key);
        }
        if(this.nodeDataImporter.setText) {
            nodeObj.text = shape.text;
            this.nodeDataImporter.setText(nodeObj.dataObj, shape.text);
        }
        if(this.nodeDataImporter.setImage) {
            nodeObj.image = shape.image.actualUrl;
            this.nodeDataImporter.setImage(nodeObj.dataObj, shape.image.actualUrl === undefined ? null : shape.image.actualUrl);
        }
        if(this.nodeDataImporter.setLeft) {
            const left = ModelUtils.getlUnitValue(units, shape.position.x);
            nodeObj.left = left;
            this.nodeDataImporter.setLeft(nodeObj.dataObj, left);
        }
        if(this.nodeDataImporter.setTop) {
            const top = ModelUtils.getlUnitValue(units, shape.position.y);
            nodeObj.top = top;
            this.nodeDataImporter.setTop(nodeObj.dataObj, top);
        }
        if(this.nodeDataImporter.setWidth) {
            const width = ModelUtils.getlUnitValue(units, shape.size.width);
            nodeObj.width = width;
            this.nodeDataImporter.setWidth(nodeObj.dataObj, width);
        }
        if(this.nodeDataImporter.setHeight) {
            const height = ModelUtils.getlUnitValue(units, shape.size.height);
            nodeObj.height = height;
            this.nodeDataImporter.setHeight(nodeObj.dataObj, height);
        }
    }
    protected updateNodeObjectConnectedProperties(shape: Shape, nodeObj: DataSourceNodeItem, changesListener?: IDataChangesListener): void {
        if(this.useNodeParentId && this.nodeDataImporter.setParentKey !== undefined) {
            const parentKey = this.getParentItemKey(shape);
            const parentItem = this.findNode(parentKey);
            this.updateNodeObjectParentKey(nodeObj, parentItem, changesListener);
        }
        if(this.useNodeContainerId && this.nodeDataImporter.setContainerKey !== undefined) {
            const containerKey = this.getContainerShapeKey(shape);
            const containerItem = this.findNode(containerKey);
            this.updateNodeObjectContainerKey(nodeObj, containerItem, changesListener);
        }
        if(this.useNodeItems && this.nodeDataImporter.setItems !== undefined) {
            const parentKey = this.getParentItemKey(shape);
            const parentItem = this.findNode(parentKey);
            this.updateNodeObjectItems(nodeObj, parentItem, changesListener);
        }
        if(this.useNodeChildren && this.nodeDataImporter.setChildren !== undefined) {
            const containerKey = this.getContainerShapeKey(shape);
            const containerItem = this.findNode(containerKey);
            this.updateNodeObjectChildren(nodeObj, containerItem, changesListener);
        }
    }
    IsNodeParentIdMode(): boolean {
        return (this.useNodeParentId && this.nodeDataImporter.setParentKey !== undefined);
    }
    IsNodeItemsMode(): boolean {
        return (this.useNodeItems && this.nodeDataImporter.setItems !== undefined);
    }
    protected updateNodeObjectParentKey(nodeObj: DataSourceNodeItem, parentNodeObj: DataSourceNodeItem,
        changesListener?: IDataChangesListener): void {
        const parentKey = this.nodeDataImporter.getParentKey(nodeObj.dataObj);
        const newParentKey = parentNodeObj ? this.nodeDataImporter.getKey(parentNodeObj.dataObj) : undefined;
        if(parentKey !== newParentKey && !(this.isRootParentKey(parentKey) && this.isRootParentKey(newParentKey))) {
            this.setDataObjectKeyRelatedProperty(this.nodeDataImporter.setParentKey, nodeObj.dataObj, newParentKey, false);
            if(changesListener)
                if(this.isInUpdateNodeKeyRelatedObjects())
                    this.addToUpdateNodeKeyRelatedObjectsStackAction("shape", nodeObj);
                else
                    this.updateNodeObjectContainerOrParentKeyInternal(nodeObj, changesListener);

        }
    }
    protected updateNodeObjectParentKeyInternal(nodeObj: DataSourceNodeItem, changesListener: IDataChangesListener): void {
        this.beginChangesNotification();
        changesListener.notifyNodeUpdated.call(changesListener,
            this.nodeDataImporter.getKey(nodeObj.dataObj) || nodeObj.key,
            nodeObj.dataObj,
            (_key: ItemDataKey, _data: any) => {
                this.endChangesNotification(false);
            },
            (_error: any) => {
                this.endChangesNotification(false);
            }
        );
    }

    protected updateNodeObjectContainerKey(nodeObj: DataSourceNodeItem, containerNodeObj: DataSourceNodeItem,
        changesListener?: IDataChangesListener): void {
        const containerKey = this.nodeDataImporter.getContainerKey(nodeObj.dataObj);
        const newContainerKey = containerNodeObj ? this.nodeDataImporter.getKey(containerNodeObj.dataObj) : undefined;
        if(containerKey !== newContainerKey && !(this.isRootParentKey(containerKey) && this.isRootParentKey(newContainerKey))) {
            this.setDataObjectKeyRelatedProperty(this.nodeDataImporter.setContainerKey, nodeObj.dataObj, newContainerKey, false);
            if(changesListener)
                if(this.isInUpdateNodeKeyRelatedObjects())
                    this.addToUpdateNodeKeyRelatedObjectsStackAction("shape", nodeObj);
                else
                    this.updateNodeObjectContainerOrParentKeyInternal(nodeObj, changesListener);
        }
    }
    protected updateNodeObjectContainerOrParentKeyInternal(nodeObj: DataSourceNodeItem, changesListener: IDataChangesListener): void {
        this.beginChangesNotification();
        changesListener.notifyNodeUpdated.call(changesListener,
            this.nodeDataImporter.getKey(nodeObj.dataObj) || nodeObj.key,
            nodeObj.dataObj,
            (_key: ItemDataKey, _data: any) => {
                this.endChangesNotification(false);
            },
            (_error: any) => {
                this.endChangesNotification(false);
            }
        );
    }

    protected isRootParentKey(key: ItemDataKey): boolean {
        return key === undefined || key === null || !this.findNode(key);
    }
    protected updateNodeObjectItems(nodeObj: DataSourceNodeItem, parentNodeObj: DataSourceNodeItem,
        changesListener?: IDataChangesListener): void {
        if(parentNodeObj && nodeObj.parentDataObj !== parentNodeObj.dataObj ||
            !parentNodeObj && nodeObj.parentDataObj)

            if(!parentNodeObj || !this.checkNodeCyrcleItems(nodeObj.dataObj, parentNodeObj.dataObj)) {
                const oldItemsArray = nodeObj.parentDataObj ? this.nodeDataImporter.getItems(nodeObj.parentDataObj) : this.nodeDataSource;
                const index = oldItemsArray.indexOf(nodeObj.dataObj);
                oldItemsArray.splice(index, 1);

                const itemsArray = parentNodeObj ? this.nodeDataImporter.getItems(parentNodeObj.dataObj) : this.nodeDataSource;
                if(!itemsArray)
                    this.nodeDataImporter.setItems(parentNodeObj.dataObj, [nodeObj.dataObj]);
                else
                    itemsArray.push(nodeObj.dataObj);
                nodeObj.parentDataObj = parentNodeObj && parentNodeObj.dataObj;

                if(changesListener) {
                    this.beginChangesNotification();
                    changesListener.notifyNodeUpdated.call(changesListener,
                        this.nodeDataImporter.getKey(nodeObj.dataObj) || nodeObj.key,
                        nodeObj.dataObj,
                        (_key: ItemDataKey, _data: any) => {
                            this.endChangesNotification(false);
                        },
                        (_error: any) => {
                            this.endChangesNotification(false);
                        }
                    );
                }
            }

    }
    protected updateNodeObjectChildren(nodeObj: DataSourceNodeItem, containerNodeObj: DataSourceNodeItem,
        changesListener?: IDataChangesListener): void {
        if(containerNodeObj && nodeObj.containerDataObj !== containerNodeObj.dataObj ||
            !containerNodeObj && nodeObj.containerDataObj) {

            const oldChildrenArray = nodeObj.containerDataObj ? this.nodeDataImporter.getChildren(nodeObj.containerDataObj) : this.nodeDataSource;
            const index = oldChildrenArray.indexOf(nodeObj.dataObj);
            oldChildrenArray.splice(index, 1);

            const childrenArray = containerNodeObj ? this.nodeDataImporter.getChildren(containerNodeObj.dataObj) : this.nodeDataSource;
            if(!childrenArray)
                this.nodeDataImporter.setChildren(containerNodeObj.dataObj, [nodeObj.dataObj]);
            else
                childrenArray.push(nodeObj.dataObj);
            nodeObj.containerDataObj = containerNodeObj && containerNodeObj.dataObj;

            if(changesListener) {
                this.beginChangesNotification();
                changesListener.notifyNodeUpdated.call(changesListener,
                    this.nodeDataImporter.getKey(nodeObj.dataObj) || nodeObj.key,
                    nodeObj.dataObj,
                    (_key: ItemDataKey, _data: any) => {
                        this.endChangesNotification(false);
                    },
                    (_error: any) => {
                        this.endChangesNotification(false);
                    }
                );
            }
        }
    }
    protected checkNodeCyrcleItems(nodeDataObj: any, parentDataObjCandidate: any): boolean {
        let result = false;
        const items = this.nodeDataImporter.getItems(nodeDataObj);
        if(items)
            items.forEach(childDataObj => {
                result = result || childDataObj === parentDataObjCandidate ||
                    this.checkNodeCyrcleItems(childDataObj, parentDataObjCandidate);
            });

        return result;
    }
    protected updateNodeObjectKey(shape: Shape, nodeObj: DataSourceNodeItem, dataObj: any): void {
        const key = this.nodeDataImporter.getKey(dataObj);
        let dataKeyChanged = false;
        if(key !== undefined && key !== null && key !== nodeObj.key) {
            delete this.autoGeneratedDataKeys[nodeObj.key];
            nodeObj.key = key;
            dataKeyChanged = true;
        }
        shape.dataKey = nodeObj.key;
        if(nodeObj.dataObj !== dataObj) {
            const parentArray = this.getNodeArray(nodeObj);
            const index = parentArray.indexOf(nodeObj.dataObj);
            parentArray.splice(index, 1, dataObj);
            nodeObj.dataObj = dataObj;
        }
        if(dataKeyChanged)
            this.updateNodeKeyRelatedObjects(shape, nodeObj);
    }
    protected updateNodeKeyRelatedObjects(shape: Shape, nodeObj: DataSourceNodeItem): void {
        if(this.isInNodeInserting()) {
            this.addToUpdateNodeKeyRelatedObjectsStack(shape, nodeObj);
            return;
        }

        if(this.useNodeParentId && this.nodeDataImporter.setParentKey !== undefined) {
            const childItems = this.getChildItems(shape);
            childItems.forEach(item => {
                const childNodeObj = this.findNode(item.dataKey);
                if(childNodeObj)
                    this.updateNodeObjectParentKey(childNodeObj, nodeObj, this.changesListener);
            });
        }
        if(this.useNodeContainerId && this.nodeDataImporter.setContainerKey !== undefined)
            shape.children.forEach(item => {
                const childNodeObj = item instanceof Shape ? this.findNode(item.dataKey) : undefined;
                if(childNodeObj)
                    this.updateNodeObjectContainerKey(childNodeObj, nodeObj, this.changesListener);
            });

        if(this.useEdgesArray())
            shape.attachedConnectors.forEach(connector => {
                const edgeObj = this.findEdge(connector.dataKey);
                if(edgeObj) {
                    if(shape === connector.beginItem)
                        this.updateEdgeObjectFromProperty(nodeObj, edgeObj, this.changesListener);
                    if(shape === connector.endItem)
                        this.updateEdgeObjectToProperty(nodeObj, edgeObj, this.changesListener);
                }
            });
    }
    protected deleteNodes(model: DiagramModel, notifications: DelayedNodeNotifications): void {
        this.deleteItems(this.nodes,
            key => model.findShapeByDataKey(key),
            item => this.getNodeArray(item),
            (item: DataSourceItem, dataModified: boolean) => {
                const key = (item.dataObj && this.nodeDataImporter.getKey(item.dataObj)) || item.key;
                const nodeObj = this.findNode(key);
                if(nodeObj)
                    this.nodes.splice(this.nodes.indexOf(nodeObj), 1);
                if(dataModified)
                    notifications.deletes.push([key, item]);
            }
        );
    }
    protected getParentItem(shape: Shape): DiagramItem {
        for(let i = 0; i < shape.attachedConnectors.length; i++)
            if(shape.attachedConnectors[i].endItem === shape)
                return shape.attachedConnectors[i].beginItem;


    }
    protected getParentItemKey(shape: Shape): ItemDataKey {
        const parent = this.getParentItem(shape);
        return parent && parent.dataKey;
    }
    protected getNodeArray(item: DataSourceNodeItem): any[] {
        let items;
        if(this.useNodeItems && item.parentDataObj)
            items = this.nodeDataImporter.getItems(item.parentDataObj);
        else if(item.containerDataObj)
            items = this.nodeDataImporter.getChildren(item.containerDataObj);
        return items || this.nodeDataSource;
    }
    protected getContainerShapeKey(shape: Shape): ItemDataKey {
        return shape.container && shape.container.dataKey;
    }
    protected getChildItems(shape: Shape): DiagramItem[] {
        const items = [];
        for(let i = 0; i < shape.attachedConnectors.length; i++)
            if(shape.attachedConnectors[i].beginItem === shape)
                if(shape.attachedConnectors[i].endItem)
                    items.push(shape.attachedConnectors[i].endItem);


        return items;
    }
    protected updateEdge(model: DiagramModel, connector: Connector): any {
        const beginDataKey = connector.beginItem ? connector.beginItem.dataKey : undefined;
        const endDataKey = connector.endItem ? connector.endItem.dataKey : undefined;

        let edgeObj = this.findEdge(connector.dataKey);
        if(!edgeObj) {
            const dataObj = this.useEdgesArray() && this.canUpdateEdgeDataSource ? {} : undefined;
            if(dataObj && connector.dataKey !== undefined && connector.dataKey !== null)
                this.edgeDataImporter.setKey(dataObj, connector.dataKey);
            edgeObj = this.addEdgeInternal(dataObj, beginDataKey, endDataKey);
            if(dataObj) {
                this.setDataObjectKeyRelatedProperty(this.edgeDataImporter.setKey, dataObj, edgeObj.key, this.addInternalKeyOnInsert);
                this.edgeDataSource.push(edgeObj.dataObj);
            }
            this.updateEdgeObjectProperties(connector, edgeObj, model.units);
            this.updateEdgeObjectKey(connector, edgeObj, edgeObj.dataObj);
            if(dataObj) {
                this.beginChangesNotification();
                this.beginNodeInserting();
                this.changesListener.notifyEdgeInserted.call(this.changesListener, edgeObj.dataObj,
                    (data: any) => {
                        this.updateEdgeObjectKey(connector, edgeObj, data);
                        this.endNodeInserting();
                        this.endChangesNotification(false);
                    },
                    (_error: any) => {
                        this.endNodeInserting();
                        this.endChangesNotification(false);
                    }
                );
            }
        }
        else if(this.isEdgeObjectModified(connector, edgeObj, model.units)) {
            this.updateEdgeObjectProperties(connector, edgeObj, model.units);
            if(edgeObj.dataObj) {
                this.beginChangesNotification();
                this.changesListener.notifyEdgeUpdated.call(this.changesListener,
                    this.edgeDataImporter.getKey(edgeObj.dataObj) || edgeObj.key, edgeObj.dataObj,
                    (_key: ItemDataKey, _data: any) => {
                        this.endChangesNotification(false);
                    },
                    (_error: any) => {
                        this.endChangesNotification(false);
                    }
                );
            }
        }

    }
    protected isEdgeObjectModified(connector: Connector, edgeObj: DataSourceEdgeItem, units: DiagramUnit): boolean {
        return this.isItemObjectModified(connector, edgeObj, this.edgeDataImporter) ||
            (edgeObj.from !== null ? edgeObj.from : undefined) !== (connector.beginItem ? connector.beginItem.dataKey : undefined) ||
            (edgeObj.to === null ? undefined : edgeObj.to) !== (connector.endItem ? connector.endItem.dataKey : undefined) ||
            (this.edgeDataImporter.setFromPointIndex && edgeObj.fromPointIndex !== connector.beginConnectionPointIndex) ||
            (this.edgeDataImporter.setToPointIndex && edgeObj.toPointIndex !== connector.endConnectionPointIndex) ||
            (this.edgeDataImporter.setPoints && (!edgeObj.points ||
                !this.pointsAreEqual(edgeObj.points.map(ptObj => ptObj.x), connector.points.map(pt => ModelUtils.getlUnitValue(units, pt.x))) ||
                !this.pointsAreEqual(edgeObj.points.map(ptObj => ptObj.y), connector.points.map(pt => ModelUtils.getlUnitValue(units, pt.y))))) ||
            (this.edgeDataImporter.setText && !this.compareTexts(edgeObj, connector)) ||
            (this.edgeDataImporter.setLineOption && edgeObj.lineOption !== connector.properties.lineOption) ||
            (this.edgeDataImporter.setStartLineEnding && edgeObj.startLineEnding !== connector.properties.startLineEnding) ||
            (this.edgeDataImporter.setEndLineEnding && edgeObj.endLineEnding !== connector.properties.endLineEnding);
    }
    pointsAreEqual(a: any[], b: any[]): boolean {
        const aLen = a.length;
        const bLen = a.length;
        if(aLen !== bLen)
            return false;
        for(let i = 0; i < aLen; i++)
            if(!MathUtils.numberCloseTo(a[i], b[i]))
                return false;
        return true;
    }
    protected updateEdgeObjectFromProperty(fromObj: DataSourceNodeItem, edgeObj: DataSourceEdgeItem,
        changesListener?: IDataChangesListener): void {
        edgeObj.from = fromObj && fromObj.key;
        if(edgeObj.dataObj) {
            const fromKey = fromObj && fromObj.dataObj && this.nodeDataImporter.getKey(fromObj.dataObj);
            this.setDataObjectKeyRelatedProperty(this.edgeDataImporter.setFrom, edgeObj.dataObj, fromKey, false);
            if(changesListener)
                if(this.isInUpdateNodeKeyRelatedObjects())
                    this.addToUpdateNodeKeyRelatedObjectsStackAction("edge", edgeObj);
                else
                    this.updateEdgeObjectFromOrToPropertyInternal(edgeObj, changesListener);
        }
    }
    protected updateEdgeObjectFromOrToPropertyInternal(edgeObj: DataSourceEdgeItem, changesListener: IDataChangesListener): void {
        this.beginChangesNotification();
        changesListener.notifyEdgeUpdated.call(changesListener,
            this.nodeDataImporter.getKey(edgeObj.dataObj) || edgeObj.key,
            edgeObj.dataObj,
            (_key: ItemDataKey, _data: any) => {
                this.endChangesNotification(false);
            },
            (_error: any) => {
                this.endChangesNotification(false);
            }
        );
    }
    protected updateEdgeObjectToProperty(toObj: DataSourceNodeItem, edgeObj: DataSourceEdgeItem,
        changesListener?: IDataChangesListener): void {
        edgeObj.to = toObj && toObj.key;
        if(edgeObj.dataObj) {
            const toKey = toObj && toObj.dataObj && this.nodeDataImporter.getKey(toObj.dataObj);
            this.setDataObjectKeyRelatedProperty(this.edgeDataImporter.setTo, edgeObj.dataObj, toKey, false);
            if(changesListener)
                if(this.isInUpdateNodeKeyRelatedObjects())
                    this.addToUpdateNodeKeyRelatedObjectsStackAction("edge", edgeObj);
                else
                    this.updateEdgeObjectFromOrToPropertyInternal(edgeObj, changesListener);
        }
    }
    protected updateEdgeObjectProperties(connector: Connector, edgeObj: DataSourceEdgeItem, units: DiagramUnit): void {
        this.updateItemObjectProperties(edgeObj, connector, this.edgeDataImporter);

        if(this.edgeDataImporter.setFrom) {
            const fromObj = this.findNode(connector.beginItem && connector.beginItem.dataKey);
            this.updateEdgeObjectFromProperty(fromObj, edgeObj);
        }
        if(this.edgeDataImporter.setTo) {
            const toObj = this.findNode(connector.endItem && connector.endItem.dataKey);
            this.updateEdgeObjectToProperty(toObj, edgeObj);
        }

        if(this.edgeDataImporter.setFromPointIndex) {
            edgeObj.fromPointIndex = connector.beginConnectionPointIndex;
            if(edgeObj.dataObj)
                this.edgeDataImporter.setFromPointIndex(edgeObj.dataObj, connector.beginConnectionPointIndex);
        }
        if(this.edgeDataImporter.setToPointIndex) {
            edgeObj.toPointIndex = connector.endConnectionPointIndex;
            if(edgeObj.dataObj)
                this.edgeDataImporter.setToPointIndex(edgeObj.dataObj, connector.endConnectionPointIndex);
        }
        if(this.edgeDataImporter.setPoints) {
            const points = connector.points.map(pt => {
                return {
                    x: ModelUtils.getlUnitValue(units, pt.x),
                    y: ModelUtils.getlUnitValue(units, pt.y)
                };
            });
            edgeObj.points = points;
            if(edgeObj.dataObj)
                this.edgeDataImporter.setPoints(edgeObj.dataObj, points);
        }

        if(this.edgeDataImporter.setText) {
            let text;
            if(connector.getTextCount() === 1 && connector.getText())
                text = connector.getText();
            const texts = {};
            connector.texts.forEach(text => {
                texts[text.position] = text.value;
            });
            edgeObj.texts = texts;
            if(edgeObj.dataObj) {
                let textVal: any = "";
                if(text)
                    textVal = text;
                else if(texts && Object.keys(texts).length)
                    textVal = texts;
                this.edgeDataImporter.setText(edgeObj.dataObj, textVal);
            }
        }
        if(this.edgeDataImporter.setLineOption) {
            edgeObj.lineOption = connector.properties.lineOption;
            if(edgeObj.dataObj)
                this.edgeDataImporter.setLineOption(edgeObj.dataObj, connector.properties.lineOption);
        }
        if(this.edgeDataImporter.setStartLineEnding) {
            edgeObj.startLineEnding = connector.properties.startLineEnding;
            if(edgeObj.dataObj)
                this.edgeDataImporter.setStartLineEnding(edgeObj.dataObj, connector.properties.startLineEnding);
        }
        if(this.edgeDataImporter.setEndLineEnding) {
            edgeObj.endLineEnding = connector.properties.endLineEnding;
            if(edgeObj.dataObj)
                this.edgeDataImporter.setEndLineEnding(edgeObj.dataObj, connector.properties.endLineEnding);
        }
    }
    protected updateEdgeObjectKey(connector: Connector, edgeObj: DataSourceEdgeItem, dataObj: any): void {
        const key = dataObj && this.edgeDataImporter.getKey(dataObj);
        if(key !== undefined && key !== null && key !== edgeObj.key) {
            delete this.autoGeneratedDataKeys[edgeObj.key];
            edgeObj.key = key;
        }
        connector.dataKey = edgeObj.key;
        if(edgeObj.dataObj !== dataObj) {
            const parentArray = this.edgeDataSource;
            const index = parentArray.indexOf(edgeObj.dataObj);
            parentArray.splice(index, 1, dataObj);
            edgeObj.dataObj = dataObj;
        }
    }
    protected deleteEdges(model: DiagramModel): void {
        this.deleteItems(this.edges,
            key => model.findConnectorByDataKey(key),
            _item => this.edgeDataSource,
            (item: DataSourceItem, dataModified: boolean) => {
                const key = (item.dataObj && this.edgeDataImporter.getKey(item.dataObj)) || item.key;
                const edgeObj = this.findEdge(key);
                if(edgeObj)
                    this.edges.splice(this.edges.indexOf(edgeObj), 1);
                if(dataModified) {
                    this.beginChangesNotification();
                    this.changesListener.notifyEdgeRemoved.call(this.changesListener, key, item.dataObj,
                        (_key: ItemDataKey, _data: any) => {
                            this.endChangesNotification(false);
                        },
                        (_error: any) => {
                            this.endChangesNotification(false);
                        }
                    );
                }
            }
        );
    }

    beginNodeInserting(): void {
        this.nodeInsertingLockCount++;
    }
    endNodeInserting(): void {
        this.nodeInsertingLockCount--;
        if(this.nodeInsertingLockCount === 0)
            this.raiseNodeInsertingStack();
    }
    isInNodeInserting(): boolean {
        return this.nodeInsertingLockCount > 0;
    }
    addToUpdateNodeKeyRelatedObjectsStack(shape: Shape, nodeObj: DataSourceNodeItem): void {
        const item = new UpdateNodeKeyRelatedObjectsStackItem(shape, nodeObj);
        this.updateNodeKeyRelatedObjectsStack.push(item);
    }
    raiseNodeInsertingStack(): void {
        this.beginUpdateNodeKeyRelatedObjects();
        while(this.updateNodeKeyRelatedObjectsStack.length > 0) {
            const item = this.updateNodeKeyRelatedObjectsStack[0];
            this.updateNodeKeyRelatedObjects(item.shape, item.nodeObj);
            this.updateNodeKeyRelatedObjectsStack.splice(0, 1);

            if(item.shape.description.hasTemplate && item.nodeObj)
                this.changesListener.reloadInsertedItem(item.nodeObj.key);
        }
        this.endUpdateNodeKeyRelatedObjects();
    }

    beginUpdateNodeKeyRelatedObjects(): void {
        this.updateNodeKeyRelatedObjectsCount++;
    }
    endUpdateNodeKeyRelatedObjects(): void {
        this.updateNodeKeyRelatedObjectsCount--;
        if(this.updateNodeKeyRelatedObjectsCount === 0)
            this.raiseUpdateNodeKeyRelatedObjectsStack();
    }
    isInUpdateNodeKeyRelatedObjects(): boolean {
        return this.updateNodeKeyRelatedObjectsCount > 0;
    }
    addToUpdateNodeKeyRelatedObjectsStackAction(kind: string, nodeObj: DataSourceNodeItem | DataSourceEdgeItem): void {
        const item = new UpdateNodeKeyRelatedObjectsStackAction(kind, nodeObj);
        for(let i = 0; i < this.updateNodeKeyRelatedObjectsStackActions.length; i++)
            if((this.updateNodeKeyRelatedObjectsStackActions[i].kind === kind) && (this.updateNodeKeyRelatedObjectsStackActions[i].nodeObj === nodeObj))
                return;
        this.updateNodeKeyRelatedObjectsStackActions.push(item);
    }
    raiseUpdateNodeKeyRelatedObjectsStack(): void {
        while(this.updateNodeKeyRelatedObjectsStackActions.length > 0) {
            const item = this.updateNodeKeyRelatedObjectsStackActions[0];
            switch(item.kind) {
                case "shape":
                    this.updateNodeObjectContainerOrParentKeyInternal(item.nodeObj, this.changesListener);
                    break;
                case "edge":
                    this.updateEdgeObjectFromOrToPropertyInternal(item.nodeObj, this.changesListener);
                    break;
            }
            this.updateNodeKeyRelatedObjectsStackActions.splice(0, 1);
        }
    }

    protected beginChangesNotification() : void {
        this.changesListener.beginChangesNotification();
    }
    protected endChangesNotification(preventNotifyChanges : boolean) : void {
        this.changesListener.endChangesNotification(preventNotifyChanges);
    }
}
