import { Size } from "@devexpress/utils/lib/geometry/size";
import { Point } from "@devexpress/utils/lib/geometry/point";

import { ModelManipulator } from "./Model/ModelManipulator";
import { CommandManager, DiagramCommand } from "./Commands/CommandManager";
import { EventManager } from "./Events/EventManager";
import { DiagramModel } from "./Model/Model";
import { Selection } from "./Selection/Selection";
import { History, IHistoryChangesListener } from "./History/History";
import { BarManager } from "./UI/BarManager";
import { RenderManager as RenderManager } from "./Render/RenderManager";
import { ShapeDescriptionManager } from "./Model/Shapes/Descriptions/ShapeDescriptionManager";
import { ItemKey, ConnectionPointSide, DiagramItem, ItemDataKey } from "./Model/DiagramItem";
import { IDataChangesListener, IEdgeDataImporter, INodeDataImporter, IDataImportParameters, IDataLayoutImportParameters } from "./Data/Interfaces";
import { DocumentDataSource } from "./Data/DocumentDataSource";
import { IToolboxDragListener, IToolboxClickListener } from "./Render/Toolbox/Toolbox";
import { DiagramSettings, IShapeSizeSettings } from "./Settings";
import { ModelOperationSettings, DiagramModelOperation, IModelOperationSettings } from "./ModelOperationSettings";
import { ViewController } from "./ViewController";
import { ICustomShape } from "./Interfaces";
import { ModelUtils } from "./Model/ModelUtils";
import { ToolboxManager } from "./Render/Toolbox/ToolboxManager";
import { EventDispatcher, GeometryUtils } from "./Utils";
import { IContextMenuVisibilityChangesListener } from "./Events/ContextMenu/ContextMenuHandler";
import { INativeActionListener } from "./Api/INativeActionListener";
import { ApiController } from "./Api/ApiController";
import { IContextToolboxVisibilityChangesListener } from "./Events/ContextToolboxHandler";
import { IScrollView } from "./Render/ScrollView";
import { IShapeIconToolboxOptions } from "./Render/Toolbox/IconToolbox";
import { ITextInputOperationListener } from "./Events/Event";
import { IBar } from "./UI/IBar";
import { IShapeDescriptionChangesListener, ShapeDescription } from "./Model/Shapes/Descriptions/ShapeDescription";
import { IImageCacheChangesListener, ImageCache } from "./Images/ImageCache";
import { ITextMeasurer } from "./Render/Measurer/ITextMeasurer";
import { IPermissionRequestListener, RequestOperationEventArgs, PermissionsProvider } from "./Model/Permissions/PermissionsProvider";
import { ConnectorRoutingModel } from "./Model/Connectors/Routing/ConnectorRoutingModel";
import { TextMeasurer } from "./Render/Measurer/TextMeasurer";
import { Connector, ConnectorPosition } from "./Model/Connectors/Connector";
import { Shape } from "./Model/Shapes/Shape";
import { DataLayoutParameters } from "./Data/DataLayoutParameters";
import { ReloadContentParameters } from "./ReloadContentParameters";
import { INativeItem } from "./Api/INativeItem";
import { RenderUtils } from "./Render/Utils";
import { ChangeConnectionEventArgs } from "./Model/Permissions/Entities/ChangeConnection";
import { Browser } from ".";
import { IDiagramController } from "./IDiagramController";
import { MathUtils } from "@devexpress/utils/lib/utils/math";

export class DiagramControl implements IHistoryChangesListener,
    IDataChangesListener, IToolboxDragListener, ITextInputOperationListener,
    IContextMenuVisibilityChangesListener, IContextToolboxVisibilityChangesListener, IToolboxClickListener,
    IShapeDescriptionChangesListener, IImageCacheChangesListener, IPermissionRequestListener, IDiagramController {

    model: DiagramModel;
    routingModel : ConnectorRoutingModel;
    modelManipulator: ModelManipulator;
    shapeDescriptionManager: ShapeDescriptionManager;
    render: RenderManager;
    history: History;
    commandManager: CommandManager;
    selection: Selection;
    eventManager: EventManager;
    barManager: BarManager;
    settings: DiagramSettings;
    view: ViewController;
    toolboxManager: ToolboxManager;
    permissionsProvider: PermissionsProvider;

    measurer: ITextMeasurer;

    private lockedReadOnly;
    private contextToolbox;
    private contextToolboxOnClick;

    apiController: ApiController;

    onNativeAction: EventDispatcher<INativeActionListener>;

    onChanged: () => void;
    onEdgeInserted: (data: any, callback?: (data: any) => void, errorCallback?: (error: any) => void) => void;
    onEdgeUpdated: (key: ItemKey, data: any, callback?: (key: ItemKey, data: any) => void, errorCallback?: (error: any) => void) => void;
    onEdgeRemoved: (key: ItemKey, data: any, callback?: (key: ItemKey, data: any) => void, errorCallback?: (error: any) => void) => void;
    onNodeInserted: (data: any, callback?: (data: any) => void, errorCallback?: (error: any) => void) => void;
    onNodeUpdated: (key: ItemKey, data: any, callback?: (key: ItemKey, data: any) => void, errorCallback?: (error: any) => void) => void;
    onNodeRemoved: (key: ItemKey, data: any, callback?: (key: ItemKey, data: any) => void, errorCallback?: (error: any) => void) => void;

    onToolboxDragStart: () => void;
    onToolboxDragEnd: () => void;
    onTextInputStart: () => void;
    onTextInputEnd: () => void;
    onToggleFullscreen: (value: boolean) => void;
    onShowContextMenu: (eventX: number, eventY: number, selection: { x: number, y: number, width: number, height: number}) => void;
    onHideContextMenu: () => void;
    onShowContextToolbox: (x: number, y: number, side: ConnectionPointSide, category: string, callback: (shapeType: string) => void) => void;
    onHideContextToolbox: () => void;

    onRequestOperation: (operation: DiagramModelOperation, args: RequestOperationEventArgs) => void;

    contextMenuPosition: Point | undefined;

    documentDataSource: DocumentDataSource;
    private updateLockCount: number = 0;
    private shouldUpdateItemsByModel = false;
    private reloadContentNeeded = false;
    private reloadContentParameters = new ReloadContentParameters();
    private reloadContentByExternalChangesParameters = new ReloadContentParameters();
    instanceId: string;

    constructor() {
        this.instanceId = MathUtils.generateGuid();
        this.settings = new DiagramSettings();
        this.shapeDescriptionManager = new ShapeDescriptionManager();
        this.shapeDescriptionManager.onShapeDecriptionChanged.add(this);
        this.model = new DiagramModel();
        this.selection = new Selection(this.model);
        this.onNativeAction = new EventDispatcher();
        this.apiController = new ApiController(this.onNativeAction, this.selection, this.model);
        this.permissionsProvider = new PermissionsProvider(this.apiController);
        this.permissionsProvider.onRequestOperation.add(this);
        this.routingModel = new ConnectorRoutingModel();
        this.modelManipulator = new ModelManipulator(this.model, this.routingModel, this.permissionsProvider, this.shapeDescriptionManager);
        this.modelManipulator.onModelChanged.add(this.permissionsProvider);

        this.history = new History(this.modelManipulator, this);

        this.barManager = new BarManager(this);
        this.view = new ViewController(this.settings, this.barManager);
        this.commandManager = new CommandManager(this);
        this.eventManager = new EventManager(this);
        this.settings.onReadOnlyChanged.add(this.eventManager.mouseHandler);
        this.settings.onReadOnlyChanged.add(this.eventManager.visualizersManager);
        this.selection.onChanged.add(this.barManager);
        this.selection.onChanged.add(this.apiController);
        this.selection.onChanged.add(this.permissionsProvider);

        this.modelManipulator.commitItemsCreateChanges();
        this.history.onChanged.add(this);
        this.toolboxManager = new ToolboxManager(this.shapeDescriptionManager);

        this.settings.onConnectorRoutingModeChanged.add(this.routingModel);

        ImageCache.instance.onReadyStateChanged.add(this);
    }

    get operationSettings() :ModelOperationSettings {
        return this.permissionsProvider.operationSettings;
    }

    cleanMarkup(removeElement?: (element: HTMLElement) => void) {
        removeElement = removeElement || ((element: HTMLElement) => { RenderUtils.removeElement(element); });
        this.toolboxManager.clean(removeElement);
        this.barManager.clean();
        if(this.render) {
            this.settings.onZoomChanged.remove(this.render.view);
            this.settings.onViewChanged.remove(this.render.page);
            this.settings.onViewChanged.remove(this.render.view);
            this.settings.onReadOnlyChanged.remove(this.render);
            this.settings.onReadOnlyChanged.remove(this.render.selection);

            this.eventManager.cleanToolboxes(this.settings.onReadOnlyChanged);
            this.eventManager.onTextInputOperation.remove(this.render.input);
            this.eventManager.onTextInputOperation.remove(this.render.items);
            this.eventManager.onTextInputOperation.remove(this.render.selection);
            this.eventManager.onMouseOperation.remove(this.render.items);
            this.eventManager.onMouseOperation.remove(this.render.selection);
            this.eventManager.onMouseOperation.remove(this.render.view);
            this.eventManager.onMouseOperation.remove(this.render);
            this.eventManager.onVisualizersUpdate.remove(this.render.selection);

            this.modelManipulator.onModelSizeChanged.remove(this.render.view);
            this.modelManipulator.onModelSizeChanged.remove(this.render.page);
            this.modelManipulator.onModelChanged.remove(this.render.items);
            this.modelManipulator.onModelChanged.remove(this.render.page);
            this.modelManipulator.onModelChanged.remove(this.render.selection);
            this.selection.onChanged.remove(this.render.selection);
            this.selection.onChanged.remove(this.render.items);

            this.render.clean(removeElement);
            this.render = undefined;
        }
        if(this.measurer && this.measurer instanceof TextMeasurer)
            this.measurer.clean();
    }

    dispose(): void {
        ImageCache.instance.onReadyStateChanged.remove(this);
    }

    createDocument(parent: HTMLElement, scrollView?: IScrollView, focusElementsParent?: HTMLElement) {
        if(!this.measurer)
            this.initMeasurer(parent);
        if(this.render)
            this.render.replaceParent(parent, scrollView);
        else {
            this.render = new RenderManager(parent, this.eventManager, this.measurer, {
                pageColor: this.model.pageColor,
                modelSize: this.model.size,
                pageLandscape: this.model.pageLandscape,
                pageSize: this.model.pageSize,
                simpleView: this.settings.simpleView,
                readOnly: this.settings.readOnly,
                contextMenuEnabled: this.settings.contextMenuEnabled,
                gridSize: this.settings.gridSize,
                gridVisible: this.settings.showGrid,
                zoomLevel: this.settings.zoomLevel,
                autoZoom: this.settings.autoZoom,
                rectangle: this.model.getRectangle(true)
            }, this.instanceId, scrollView, focusElementsParent);
            this.settings.onZoomChanged.add(this.render.view);
            this.settings.onViewChanged.add(this.render.page);
            this.settings.onViewChanged.add(this.render.view);
            this.settings.onReadOnlyChanged.add(this.render);
            this.settings.onReadOnlyChanged.add(this.render.selection);

            this.eventManager.onTextInputOperation.add(this.render.input);
            this.eventManager.onTextInputOperation.add(this.render.items);
            this.eventManager.onTextInputOperation.add(this.render.selection);
            this.eventManager.onTextInputOperation.add(this);
            this.eventManager.onMouseOperation.add(this.render.items);
            this.eventManager.onMouseOperation.add(this.render.selection);
            this.eventManager.onMouseOperation.add(this.render.view);
            this.eventManager.onMouseOperation.add(this.render);
            this.eventManager.onVisualizersUpdate.add(this.render.selection);

            this.modelManipulator.onModelSizeChanged.add(this.render.view);
            this.modelManipulator.onModelSizeChanged.add(this.render.page);
            this.modelManipulator.onModelChanged.add(this.render.items);
            this.modelManipulator.onModelChanged.add(this.render.page);
            this.modelManipulator.onModelChanged.add(this.render.selection);
            this.selection.onChanged.add(this.render.selection);
            this.selection.onChanged.add(this.render.items);
            this.render.update(false);
            this.render.onNewModel(this.model.items);
            this.modelManipulator.commitItemsCreateChanges();
            this.view.initialize(this.render.view);
            if(this.settings.zoomLevelWasChanged)
                this.raiseCanvasViewActualZoomChanged();
            this.selection.raiseSelectionChanged();
        }
    }

    createToolbox(parent: HTMLElement, renderAsText: boolean, shapes: string | string[], options?: IShapeIconToolboxOptions) {
        const toolbox = this.toolboxManager.create(parent, this.settings.readOnly, true, renderAsText,
            shapes, this.getToolboxAllowedShapeTypes.bind(this), this.instanceId, options);
        this.settings.onReadOnlyChanged.add(toolbox);
        toolbox.onDragOperation.add(this);
        toolbox.onDragOperation.add(this.apiController);
        this.eventManager.registerToolbox(toolbox);
    }
    createContextToolbox(parent: HTMLElement, renderAsText: boolean, shapes: string | string[], options?: IShapeIconToolboxOptions, onClick?: (shapeType: string) => void) {
        this.cleanContextToolbox();
        this.contextToolbox = this.toolboxManager.create(parent, this.settings.readOnly, false, renderAsText,
            shapes, this.getToolboxAllowedShapeTypes.bind(this), this.instanceId, options);
        this.contextToolbox.onClickOperation.add(this);
        this.contextToolboxOnClick = onClick;
    }
    private getToolboxAllowedShapeTypes(shapeTypes: string[]): string[] {
        const allowedShapeTypes = [];
        this.permissionsProvider.beginUpdateUI();
        shapeTypes.forEach(shapeType => {
            if(this.permissionsProvider.canAddShapeFromToolbox(shapeType))
                allowedShapeTypes.push(shapeType);
        });
        this.permissionsProvider.endUpdateUI();
        return allowedShapeTypes;
    }
    cleanContextToolbox() {
        if(this.contextToolbox) {
            this.toolboxManager.clean(undefined, this.contextToolbox);
            this.contextToolbox = undefined;
            this.contextToolboxOnClick = undefined;
        }
    }
    refreshToolbox(toolboxes?: number[]): void {
        this.permissionsProvider.clearCache(DiagramModelOperation.AddShapeFromToolbox);
        this.toolboxManager.refresh(toolboxes);
    }
    applyToolboxFilter(shapeSubstring: string, toolboxes?: number[]): number[] {
        return this.toolboxManager.applyFilter(shapeSubstring, toolboxes);
    }
    notifyToolboxClick(shapeType: string) {
        if(this.contextToolboxOnClick)
            this.contextToolboxOnClick(shapeType);
    }

    initMeasurer(parent: HTMLElement) {
        this.measurer = new TextMeasurer(parent);
    }
    onDimensionChanged() : void {
        if(!Browser.TouchUI)
            this.updateLayout(true);
    }
    updateLayout(resetScroll = false) {
        this.render && this.render.update(!resetScroll);
    }
    captureFocus() {
        this.render && this.render.input.captureFocus();
    }
    isFocused() {
        return !this.render || this.render.input.isFocused();
    }

    registerBar(bar: IBar) {
        this.barManager.registerBar(bar);
    }
    updateBarItemsState(bar: IBar, queryCommands?: DiagramCommand[]) {
        this.barManager.updateBarItemsState(bar, queryCommands);
    }

    getCommand(key: DiagramCommand) {
        return this.commandManager.getCommand(key);
    }

    getNativeItemByKey(key: ItemKey): INativeItem {
        const item = this.model.findItem(key);
        return item && this.apiController.createNativeItem(item);
    }
    getNativeItemByDataKey(dataKey: ItemDataKey): INativeItem {
        const item = this.model.findItemByDataKey(dataKey);
        return item && this.apiController.createNativeItem(item);
    }
    getNativeItems(): INativeItem[] {
        return this.model.items.map(item => this.apiController.createNativeItem(item));
    }
    getNativeSelectedItems(): INativeItem[] {
        return this.selection.getKeys().map(key => this.apiController.createNativeItem(this.model.findItem(key)));
    }
    setSelectedItems(keys: ItemKey[]) {
        this.selection.set(keys);
    }
    scrollToItems(keys: ItemKey[]) {
        const rectangle = GeometryUtils.getCommonRectangle(keys.map(key => this.model.findItem(key).rectangle));
        this.view.scrollIntoView(rectangle);
    }

    setInitialStyleProperties(style: any) {
        this.selection.inputPosition.setInitialStyleProperties(style);
    }
    setInitialTextStyleProperties(style: any) {
        this.selection.inputPosition.setInitialTextStyleProperties(style);
    }
    setInitialConnectorProperties(properties: any) {
        this.selection.inputPosition.setInitialConnectorProperties(properties);
    }

    addCustomShapes(shapes: ICustomShape[]) {
        shapes.forEach(shape => {
            shape.apiController = this.apiController;

            if(shape.defaultWidth)
                shape.defaultWidth = ModelUtils.getTwipsValue(this.model.units, shape.defaultWidth);
            if(shape.defaultHeight)
                shape.defaultHeight = ModelUtils.getTwipsValue(this.model.units, shape.defaultHeight);
            if(shape.minWidth)
                shape.minWidth = ModelUtils.getTwipsValue(this.model.units, shape.minWidth);
            if(shape.minHeight)
                shape.minHeight = ModelUtils.getTwipsValue(this.model.units, shape.minHeight);
            if(shape.maxWidth)
                shape.maxWidth = ModelUtils.getTwipsValue(this.model.units, shape.maxWidth);
            if(shape.maxHeight)
                shape.maxHeight = ModelUtils.getTwipsValue(this.model.units, shape.maxHeight);

            this.shapeDescriptionManager.registerCustomShape(shape);
        });
    }
    removeCustomShapes(shapeTypes: string[]) {
        shapeTypes.forEach(shapeType => {
            this.shapeDescriptionManager.unregisterCustomShape(shapeType);
        });
    }
    removeAllCustomShapes() {
        this.shapeDescriptionManager.unregisterAllCustomShapes();
    }

    importModel(model: DiagramModel) {
        model.units = this.model.units; 
        this.model = model;
        this.model.initializeKeyCounter();
        this.apiController.model = model;
        this.onImportData();
    }
    importItemsData() {
        this.onImportData();
    }
    protected onImportData() {
        if(this.render) {
            this.render.clear();
            this.render.onNewModel(this.model.items);
        }
        this.permissionsProvider.clearCache();
        this.selection.initialize(this.model);
        this.modelManipulator.initialize(this.model, this.routingModel);
        this.history.clear();
        this.eventManager.initialize();
        this.modelManipulator.commitPageChanges();
        this.modelManipulator.commitItemsCreateChanges();
        this.notifyViewChanged();
        this.notifyHistoryChanged();
    }

    createDocumentDataSource(nodeDataSource: any[], edgeDataSource: any[], parameters?: IDataImportParameters,
        nodeDataImporter?: INodeDataImporter, edgeDataImporter?: IEdgeDataImporter): DocumentDataSource {
        this.documentDataSource = new DocumentDataSource(this, nodeDataSource, edgeDataSource, parameters, nodeDataImporter, edgeDataImporter);
        this.apiController.setDataSource(this.documentDataSource);
        return this.documentDataSource;
    }
    deleteDocumentDataSource() {
        this.apiController.setDataSource(null);
        delete this.documentDataSource;
    }

    applyShapeSizeSettings(settings: IShapeSizeSettings): void {
        this.settings.applyShapeSizeSettings(settings, this.model.units);
    }

    applyOperationSettings(settings: IModelOperationSettings): void {
        this.permissionsProvider.operationSettings.applySettings(settings);
    }

    beginUpdateCanvas(): void {
        if(this.render) {
            this.render.items.beginUpdate();
            this.render.selection.beginUpdate();
        }
    }
    endUpdateCanvas(): void {
        if(this.render) {
            this.render.items.endUpdate();
            this.render.selection.endUpdate();
        }
    }
    beginUpdate() {
        this.barManager.beginUpdate();
        this.apiController.beginUpdate();
        this.eventManager.beginUpdate();
    }
    endUpdate() {
        this.barManager.endUpdate();
        this.apiController.endUpdate();
        this.eventManager.endUpdate();
    }

    notifyEdgeInserted(data: any, callback?: (data: any) => void, errorCallback?: (error: any) => void) {
        if(this.onEdgeInserted)
            this.onEdgeInserted(data, callback, errorCallback);
        else
            callback(data);
    }
    notifyEdgeUpdated(key: ItemKey, data: any, callback?: (key: ItemKey, data: any) => void, errorCallback?: (error: any) => void) {
        if(this.onEdgeUpdated)
            this.onEdgeUpdated(key, data, callback, errorCallback);
        else
            callback(key, data);
    }
    notifyEdgeRemoved(key: ItemKey, data: any, callback?: (key: ItemKey, data: any) => void, errorCallback?: (error: any) => void) {
        if(this.onEdgeUpdated)
            this.onEdgeRemoved(key, data, callback, errorCallback);
        else
            callback(key, data);
    }
    notifyNodeInserted(data: any, callback?: (data: any) => void, errorCallback?: (error: any) => void) {
        if(this.onNodeInserted)
            this.onNodeInserted(data, callback, errorCallback);
        else
            callback(data);
    }
    notifyNodeUpdated(key: ItemKey, data: any, callback?: (key: ItemKey, data: any) => void, errorCallback?: (error: any) => void) {
        if(this.onNodeUpdated)
            this.onNodeUpdated(key, data, callback, errorCallback);
        else
            callback(key, data);
    }
    notifyNodeRemoved(key: ItemKey, data: any, callback?: (key: ItemKey, data: any) => void, errorCallback?: (error: any) => void) {
        if(this.onNodeRemoved)
            this.onNodeRemoved(key, data, callback, errorCallback);
        else
            callback(key, data);
    }
    reloadInsertedItem(dataKey: ItemDataKey) {
        if(this.settings.reloadInsertedItemRequired)
            this.reloadContent(dataKey);
    }

    reloadContent(dataKey?: ItemDataKey|ItemDataKey[], getData?: () => { nodeDataSource?: any[], edgeDataSource?: any[] },
        layoutParameters?: IDataLayoutImportParameters, isExternalChanges?: boolean): void {
        if(!this.documentDataSource) return;

        if(this.isChangesLocked())
            this.reloadContentNeeded = true;

        const addNewHistoryItem = isExternalChanges === true || (isExternalChanges === undefined && !this.reloadContentNeeded);

        const reloadContentParameters = addNewHistoryItem ? this.reloadContentByExternalChangesParameters : this.reloadContentParameters;
        reloadContentParameters.add(dataKey, getData, layoutParameters);

        if(!this.isChangesLocked()) {
            this.reloadContentCore(reloadContentParameters, addNewHistoryItem);
            this.barManager.updateItemsState();
        }
    }
    private reloadContentCore(parameters: ReloadContentParameters, addNewHistoryItem?: boolean): void {
        const data = parameters.getData && parameters.getData();
        const changes = this.documentDataSource.refetchData(data && data.nodeDataSource, data && data.edgeDataSource);
        this.beginUpdateCanvas();
        this.permissionsProvider.lockPermissions();
        this.documentDataSource.updateModelItems(this.history, this.model, this.shapeDescriptionManager,
            this.selection, new DataLayoutParameters(this.settings, parameters.layoutParameters),
            addNewHistoryItem, parameters.dataKeys, (item) => {
                this.modelManipulator.commitItemUpdateChanges(item);
            },
            changes, this.settings.snapToGrid, this.settings.gridSize, this.measurer
        );
        this.permissionsProvider.unlockPermissions();
        this.endUpdateCanvas();
        parameters.clear();
    }

    notifyHistoryChanged() {
        if(this.documentDataSource) {
            this.shouldUpdateItemsByModel = true;
            if(!this.settings.readOnly)
                this.notifyDataChanges();
        }
        else
            this.raiseOnChanged();
    }
    notifyViewChanged() {
        this.settings.notifyViewChanged();
    }
    notifyToolboxDragStart(evt: MouseEvent) {
        this.render.notifyToolboxDragStart(evt);
        if(this.onToolboxDragStart)
            this.onToolboxDragStart();
    }
    notifyToolboxDragEnd(evt: MouseEvent) {
        this.render.notifyToolboxDragEnd(evt);
        if(this.onToolboxDragEnd)
            this.onToolboxDragEnd();
    }
    notifyToolboxDraggingMouseMove(evt: MouseEvent) {
        if(this.render)
            this.render.notifyToolboxDraggingMouseMove(evt);
    }

    notifyTextInputStart(item: DiagramItem, text: string, position: Point, size?: Size): void {
        if(this.onTextInputStart)
            this.onTextInputStart();
    }
    notifyTextInputEnd(item: DiagramItem, captureFocus?: boolean): void {
        if(this.onTextInputEnd)
            this.onTextInputEnd();
    }
    notifyTextInputPermissionsCheck(item: DiagramItem, allowed: boolean): void {}

    notifyToggleFullscreen(value: boolean) {
        if(this.onToggleFullscreen)
            this.onToggleFullscreen(value);
    }

    notifyShowContextMenu(eventPoint: Point, modelPoint: Point) {
        if(this.onShowContextMenu && this.render) {
            let selection: any;
            const selectedItems = this.selection.getSelectedItems(true);
            if(selectedItems.length > 0) {
                const rect = ModelUtils.createRectangle(this.selection.getSelectedItems(true));
                const pos = this.render.getEventPointByModelPoint(rect.createPosition());
                const size = this.render.view.getAbsoluteSize(rect.createSize());
                selection = { x: pos.x, y: pos.y, width: size.width, height: size.height };
            }
            if(eventPoint) {
                this.contextMenuPosition = new Point(eventPoint.x, eventPoint.y);
                this.onShowContextMenu(eventPoint.x, eventPoint.y, selection);
            }
            else if(modelPoint) {
                const point = this.render.getEventPointByModelPoint(modelPoint);
                this.contextMenuPosition = point.clone();
                this.onShowContextMenu(point.x, point.y, selection);
            }
        }
    }
    notifyHideContextMenu() {
        if(this.onHideContextMenu && this.render)
            this.onHideContextMenu();
    }

    notifyShowContextToolbox(modelPoint: Point, getPositionToInsertShapeTo: (shape: Shape) => Point, side: ConnectionPointSide, category: string, callback: (shapeType: string) => void) {
        if(this.onShowContextToolbox && this.render) {
            const point = this.render.getEventPointByModelPoint(modelPoint);
            this.onShowContextToolbox(point.x, point.y, side, category, callback);
            this.render.view.notifyShowContextToolbox();
        }
    }
    notifyHideContextToolbox() {
        if(this.onHideContextToolbox && this.render) {
            this.onHideContextToolbox();
            this.render.view.notifyHideContextToolbox();
        }
        this.cleanContextToolbox();
    }

    notifyShapeDescriptionChanged(description: ShapeDescription) {
        this.modelManipulator.updateShapeDescription(description);
    }

    notifyImageCacheReadyStateChanged(ready: boolean) {
        this.barManager.updateItemsState();
    }

    raiseCanvasViewActualZoomChanged() {
        this.render.view.raiseActualZoomChanged();
    }

    notifyRequestOperation(operation: DiagramModelOperation, args: RequestOperationEventArgs) {
        if(this.requestOperationByDataSource(operation, args))
            return;

        if(this.onRequestOperation)
            this.onRequestOperation(operation, args);
    }
    private requestOperationByDataSource(operation: DiagramModelOperation, args: RequestOperationEventArgs) {
        if(!(this.documentDataSource && (this.documentDataSource.IsNodeParentIdMode() || this.documentDataSource.IsNodeItemsMode())))
            return false;

        if(operation === DiagramModelOperation.ChangeConnection) {
            const e = args as ChangeConnectionEventArgs;
            const shape = e.shape && this.model.findItem(e.shape.id);
            const connector = e.connector && <Connector> this.model.findItem(e.connector.id);
            if(!(shape && connector)) return;

            if(e.position === ConnectorPosition.End)
                for(let i = 0; i < shape.attachedConnectors.length; i++) {
                    const attachedConnector = shape.attachedConnectors[i];
                    if(attachedConnector !== connector && attachedConnector.endItem && attachedConnector.endItem === shape) {
                        e.allowed = false;
                        break;
                    }
                }

            if(e.allowed && connector.beginItem && connector.endItem && this.isShapeParent(connector.endItem, connector.beginItem))
                e.allowed = false;
        }
        return !args.allowed;
    }
    private isShapeParent(parentShape: DiagramItem, shape: DiagramItem) {
        if(parentShape === shape)
            return true;

        for(let i = 0; i < parentShape.attachedConnectors.length; i++) {
            const attachedConnector = parentShape.attachedConnectors[i];
            if(attachedConnector.beginItem === parentShape && attachedConnector.endItem) {
                const childShape = attachedConnector.endItem;
                if(childShape === shape || this.isShapeParent(childShape, shape))
                    return true;
            }
        }

        return false;
    }

    isChangesLocked(): boolean {
        return this.updateLockCount > 0;
    }
    beginChangesNotification() {
        if(!this.isChangesLocked())
            if(this.changesLockChanged)
                this.changesLockChanged(true);
        this.updateLockCount++;
    }
    endChangesNotification(preventNotifyReloadContent : boolean) {
        this.updateLockCount--;
        if(!this.isChangesLocked()) {
            this.changesLockChanged(false);
            if(!preventNotifyReloadContent)
                setTimeout(() => {
                    this.notifyReloadContent();
                    this.notifyDataChanges();
                }, 0);
        }
    }
    private changesLockChanged(locked: boolean) : void {
        if(locked)
            this.lockedReadOnly = this.settings.readOnly;
        else
            locked = this.lockedReadOnly;
        this.commandManager.getCommand(DiagramCommand.ToggleReadOnly).execute(locked);
    }
    private notifyDataChanges() {
        if(this.isChangesLocked())
            return;
        if(this.shouldUpdateItemsByModel) {
            this.documentDataSource.updateItemsByModel(this.model);
            this.shouldUpdateItemsByModel = false;
        }
        this.raiseOnChanged();
    }
    private notifyReloadContent(): void {
        if(this.reloadContentNeeded) {
            if(!this.reloadContentParameters.empty)
                this.reloadContentCore(this.reloadContentParameters, false);
            if(!this.reloadContentByExternalChangesParameters.empty)
                this.reloadContentCore(this.reloadContentByExternalChangesParameters, true);
            this.reloadContentNeeded = false;
        }
    }
    private raiseOnChanged() {
        if(this.onChanged)
            this.onChanged();
    }
}
