import { ItemChange, ItemChangeType } from "../Model/ModelChange";
import { IMouseOperationsListener, MouseEventElementType, ITextInputOperationListener } from "../Events/Event";
import { Size } from "@devexpress/utils/lib/geometry/size";
import { Point } from "@devexpress/utils/lib/geometry/point";
import { DiagramItem, ItemKey } from "../Model/DiagramItem";
import { SvgPrimitive } from "./Primitives/Primitive";
import { GroupPrimitive } from "./Primitives/GroupPrimitive";
import { RenderUtils } from "./Utils";
import { Shape } from "../Model/Shapes/Shape";
import { Connector } from "../Model/Connectors/Connector";
import { ICanvasViewListener } from "./CanvasViewManager";
import { DOMManipulator } from "./DOMManipulator";
import { DomUtils } from "@devexpress/utils/lib/utils/dom";
import { CanvasManager } from "./CanvasManager";
import { ISelectionChangesListener, Selection } from "../Selection/Selection";
import { ModelUtils } from "../Model/ModelUtils";

export const NOT_VALID_CSSCLASS = "not-valid";
export const CONNECTOR_CAN_MOVE = "can-move";

export class CanvasItemsManager extends CanvasManager implements IMouseOperationsListener,
    ITextInputOperationListener, ICanvasViewListener, ISelectionChangesListener {
    itemSelectorsContainer: SVGElement;
    itemsContainer: SVGElement;

    private itemSelectorGroupContainers: { [key: number]: SVGElement } = {};
    private itemSelectorElements: { [key: number]: SVGElement } = {};
    private itemGroupContainers: { [key: number]: SVGElement } = {};
    private itemElements: { [key: number]: SVGElement } = {};
    private itemChildElements: { [key: number]: SVGElement } = {};

    private primitives: { [key: number]: SvgPrimitive<SVGGraphicsElement>[] } = {};
    private selectorPrimitives: { [key: number]: SvgPrimitive<SVGGraphicsElement>[] } = {};

    private selectedItems : { [key: string] : DiagramItem } = {};

    constructor(viewElement: SVGElement, zoomLevel: number, dom: DOMManipulator, instanceId: string) {
        super(zoomLevel, dom, instanceId);
        this.initializeContainerElements(viewElement);
    }

    private initializeContainerElements(view: SVGElement) {
        this.itemSelectorsContainer = this.createAndChangePrimitiveElement(
            new GroupPrimitive([], null), view
        );
        this.itemsContainer = this.createAndChangePrimitiveElement(
            new GroupPrimitive([], null), view
        );
    }
    clear(): void {
        this.primitives = {};
        this.selectorPrimitives = {};

        RenderUtils.removeContent(this.itemSelectorsContainer);
        RenderUtils.removeContent(this.itemsContainer);
        this.itemSelectorGroupContainers = {};
        this.itemSelectorElements = {};
        this.itemGroupContainers = {};
        this.itemElements = {};
        this.itemChildElements = {};
        this.selectedItems = {};
    }
    applyChange(change: ItemChange): boolean {
        const item = change.item;
        const itemParent = this.getItemParent(item.zIndex, item.container && item.container.key);
        if(!itemParent) return false;

        if(item instanceof Shape)
            this.applyShapeChange(item, change.type, change.isValid);
        else if(item instanceof Connector)
            this.applyConnectorChange(item, change.type, change.isValid);
        return true;
    }

    private setPointerEventsNone(element: HTMLElement, value: boolean) {
        const style = element.style;
        if(style !== undefined && style.pointerEvents !== undefined)
            style.pointerEvents = value ? "none" : "";
        const childNodes = element.childNodes;
        for(let i = 0; i < childNodes.length; i++)
            this.setPointerEventsNone(childNodes[i] as HTMLElement, value);
    }

    notifyDragStart(itemKeys: ItemKey[]): void {
        itemKeys.forEach(itemKey => {
            if(this.itemElements[itemKey])
                this.setPointerEventsNone(this.itemElements[itemKey], true);
            if(this.itemChildElements[itemKey])
                this.setPointerEventsNone(this.itemChildElements[itemKey], true);
            if(this.itemSelectorElements[itemKey])
                this.setPointerEventsNone(this.itemSelectorElements[itemKey], true);
        });
    }
    notifyDragEnd(itemKeys: ItemKey[]): void {
        itemKeys.forEach(itemKey => {
            if(this.itemElements[itemKey])
                this.setPointerEventsNone(this.itemElements[itemKey], false);
            if(this.itemChildElements[itemKey])
                this.setPointerEventsNone(this.itemChildElements[itemKey], false);
            if(this.itemSelectorElements[itemKey])
                this.setPointerEventsNone(this.itemSelectorElements[itemKey], false);
        });
    }
    notifyDragScrollStart(): void { }
    notifyDragScrollEnd(): void { }

    notifyTextInputStart(item: DiagramItem, _text: string, _position: Point, _size?: Size): void {
        const element = this.itemElements[item.key];
        DomUtils.addClassName(element, "text-input");
    }
    notifyTextInputEnd(item: DiagramItem, _captureFocus?: boolean): void {
        const element = this.itemElements[item.key];
        DomUtils.removeClassName(element, "text-input");
    }
    notifyTextInputPermissionsCheck(item: DiagramItem, allowed: boolean): void {
        const element = this.itemElements[item.key];
        if(allowed)
            DomUtils.removeClassName(element, NOT_VALID_CSSCLASS);
        else
            DomUtils.addClassName(element, NOT_VALID_CSSCLASS);
    }

    notifyActualZoomChanged(actualZoom: number): void {
        const scale = "scale(" + actualZoom + ")";
        this.dom.changeByFunc(this.itemsContainer, e => e.setAttribute("transform", scale));
        this.dom.changeByFunc(this.itemSelectorsContainer, e => e.setAttribute("transform", scale));
        this.actualZoom = actualZoom;
    }
    notifyViewAdjusted(_canvasOffset: Point): void { }

    invalidatePrimitives(item: DiagramItem): void {
        if(this.primitives[item.key]) {
            this.primitives[item.key].forEach(primitive => {
                primitive.dispose();
            });
            delete this.primitives[item.key];
        }
        if(this.selectorPrimitives[item.key]) {
            this.selectorPrimitives[item.key].forEach(primitive => {
                primitive.dispose();
            });
            delete this.selectorPrimitives[item.key];
        }
    }
    getPrimitives(item: DiagramItem, instanceId: string): SvgPrimitive<SVGElement>[] {
        if(!this.primitives[item.key])
            this.primitives[item.key] = item.createPrimitives(instanceId);
        return this.primitives[item.key];
    }
    getSelectorPrimitives(item: DiagramItem): SvgPrimitive<SVGElement>[] {
        if(!this.selectorPrimitives[item.key])
            this.selectorPrimitives[item.key] = item.createSelectorPrimitives();
        return this.selectorPrimitives[item.key];
    }

    private getShapeSelectorClassName(shape: Shape): string {
        let className = "shape";
        if(shape.enableChildren)
            className += " container";
        if(shape.isLocked)
            className += " locked";
        return className;
    }
    private getShapeClassName(shape: Shape, isValid: boolean): string {
        const selectorClassName = this.getShapeSelectorClassName(shape);
        return isValid ? selectorClassName : selectorClassName + " " + NOT_VALID_CSSCLASS;
    }
    applyShapeChange(shape: Shape, type: ItemChangeType, isValid: boolean): void {
        const key = shape.key;
        const containerKey = shape.container && shape.container.key;
        const itemSelectorParent = this.getItemSelectorGroupContainer(shape.zIndex, containerKey);
        const itemParent = this.getItemGroupContainer(shape.zIndex, containerKey);
        const itemClassName = this.getShapeClassName(shape, isValid);
        switch(type) {
            case ItemChangeType.Create:
                this.itemSelectorElements[key] = this.createItemElements(key, this.getSelectorPrimitives(shape),
                    itemSelectorParent, this.getShapeSelectorClassName(shape), MouseEventElementType.Shape);
                this.itemElements[key] = this.createItemElements(key, this.getPrimitives(shape, this.instanceId),
                    itemParent, itemClassName, MouseEventElementType.Shape);
                if(shape.enableChildren) {
                    this.itemChildElements[key] = this.createItemElements(key, [],
                        itemParent, "container-children", MouseEventElementType.Undefined);
                    this.changeItemChildrenVisibility(this.itemChildElements[key], shape.expanded);
                }
                break;
            case ItemChangeType.Remove:
                this.removeItemCustomContent(this.itemSelectorElements[key], this.getSelectorPrimitives(shape));
                this.removeItemCustomContent(this.itemElements[key], this.getPrimitives(shape, this.instanceId));

                this.invalidatePrimitives(shape);

                this.removeItemElements(this.itemSelectorElements[key]);
                delete this.itemSelectorElements[key];
                this.removeItemElements(this.itemElements[key]);
                delete this.itemElements[key];
                if(this.itemChildElements[key]) {
                    this.removeItemElements(this.itemChildElements[key]);
                    delete this.itemChildElements[key];
                    delete this.itemGroupContainers[key];
                    delete this.itemSelectorGroupContainers[key];
                }
                break;
            case ItemChangeType.UpdateStructure:
            case ItemChangeType.UpdateProperties:
            case ItemChangeType.Update:
                if(type !== ItemChangeType.Update) {
                    if(type === ItemChangeType.UpdateStructure) {
                        this.removeItemCustomContent(this.itemSelectorElements[key], this.getSelectorPrimitives(shape));
                        this.removeItemCustomContent(this.itemElements[key], this.getPrimitives(shape, this.instanceId));
                    }
                    this.invalidatePrimitives(shape);
                }
                this.changeItemElements(this.getSelectorPrimitives(shape), this.itemSelectorElements[key],
                    type === ItemChangeType.UpdateStructure);
                this.changeItemElements(this.getPrimitives(shape, this.instanceId), this.itemElements[key],
                    type === ItemChangeType.UpdateStructure);
                this.changeItemClassName(this.itemElements[key], itemClassName);
                if(this.itemChildElements[key])
                    this.changeItemChildrenVisibility(this.itemChildElements[key], shape.expanded);


                if(itemSelectorParent !== (this.itemSelectorElements[key] && this.itemSelectorElements[key].parentNode))
                    this.moveItemElements(itemSelectorParent, this.itemSelectorElements[key]);
                if(itemParent !== (this.itemElements[key] && this.itemElements[key].parentNode))
                    this.moveItemElements(itemParent, this.itemElements[key]);
                if(this.itemChildElements[key] && (itemParent !== this.itemChildElements[key].parentNode))
                    this.moveItemElements(itemParent, this.itemChildElements[key]);
                break;
            case ItemChangeType.UpdateClassName:
                this.changeItemClassName(this.itemElements[key], itemClassName);
        }
    }

    private getConnectorSelectorClassName(connector: Connector): string {
        const selectorClassName = "connector";
        return ModelUtils.canMoveConnector(this.selectedItems, connector) ? selectorClassName + " " + CONNECTOR_CAN_MOVE : selectorClassName;
    }
    private getConnectorClassName(connector: Connector, isValid: boolean): string {
        const selectorMoveClassName = this.getConnectorSelectorClassName(connector);
        return isValid ? selectorMoveClassName : selectorMoveClassName + " " + NOT_VALID_CSSCLASS;
    }
    applyConnectorChange(connector: Connector, type: ItemChangeType, isValid: boolean): void {
        const key = connector.key;
        const containerKey = connector.container && connector.container.key;
        const itemSelectorParent = this.getItemSelectorGroupContainer(connector.zIndex, containerKey);
        const itemParent = this.getItemGroupContainer(connector.zIndex, containerKey);
        const className = this.getConnectorClassName(connector, isValid);
        switch(type) {
            case ItemChangeType.Create:
                this.itemSelectorElements[key] = this.createItemElements(key, this.getSelectorPrimitives(connector),
                    itemSelectorParent, this.getConnectorSelectorClassName(connector), MouseEventElementType.Connector);
                this.itemElements[key] = this.createItemElements(key, this.getPrimitives(connector, this.instanceId),
                    itemParent, className, MouseEventElementType.Connector);
                break;
            case ItemChangeType.Remove:
                this.removeItemCustomContent(this.itemSelectorElements[key], this.getSelectorPrimitives(connector));
                this.removeItemCustomContent(this.itemElements[key], this.getPrimitives(connector, this.instanceId));

                this.invalidatePrimitives(connector);

                this.removeItemElements(this.itemSelectorElements[key]);
                delete this.itemSelectorElements[key];
                this.removeItemElements(this.itemElements[key]);
                delete this.itemElements[key];
                break;
            case ItemChangeType.UpdateStructure:
            case ItemChangeType.UpdateProperties:
            case ItemChangeType.Update:
                if(type !== ItemChangeType.Update) {
                    if(type === ItemChangeType.UpdateStructure) {
                        this.removeItemCustomContent(this.itemSelectorElements[key], this.getSelectorPrimitives(connector));
                        this.removeItemCustomContent(this.itemElements[key], this.getPrimitives(connector, this.instanceId));
                    }
                    this.invalidatePrimitives(connector);
                }
                this.changeItemElements(this.getSelectorPrimitives(connector), this.itemSelectorElements[key],
                    type === ItemChangeType.UpdateStructure);
                this.changeItemClassName(this.itemSelectorElements[key], this.getConnectorSelectorClassName(connector));
                this.changeItemElements(this.getPrimitives(connector, this.instanceId), this.itemElements[key],
                    type === ItemChangeType.UpdateStructure);
                this.changeItemClassName(this.itemElements[key], className);
                if(itemSelectorParent !== (this.itemSelectorElements[key] && this.itemSelectorElements[key].parentNode))
                    this.moveItemElements(itemSelectorParent, this.itemSelectorElements[key]);
                if(itemParent !== (this.itemElements[key] && this.itemElements[key].parentNode))
                    this.moveItemElements(itemParent, this.itemElements[key]);
                break;
            case ItemChangeType.UpdateClassName:
                this.changeItemClassName(this.itemSelectorElements[key], this.getConnectorSelectorClassName(connector));
                this.changeItemClassName(this.itemElements[key], className);
        }
    }
    createItemElements(key: ItemKey, primitives: SvgPrimitive<SVGElement>[], parent: SVGElement,
        className: string, type: MouseEventElementType): SVGElement {
        const gEl = this.createAndChangePrimitiveElement(new GroupPrimitive([], className, undefined, undefined, e => RenderUtils.setElementEventData(e, type, key)), parent);
        this.createAndChangePrimitivesElements(primitives, gEl);
        return gEl;
    }
    changeItemElements(primitives: SvgPrimitive<SVGElement>[], element: SVGElement, updateStructure: boolean): void {
        if(updateStructure || primitives.length !== element.childNodes.length) {
            RenderUtils.removeContent(element);
            this.createAndChangePrimitivesElements(primitives, element);
        }
        else
            this.dom.changeChildrenByPrimitives(primitives, element);
    }
    changeItemClassName(element: SVGElement, className?: string): void {
        if(className && element)
            this.changePrimitiveElement(new GroupPrimitive([], className), element);
    }
    removeItemElements(element: SVGElement): void {
        if(element && element.parentNode)
            element.parentNode.removeChild(element);
    }
    removeItemCustomContent(element: SVGElement, primitives: SvgPrimitive<SVGElement>[]): void {
        if(element && primitives && primitives.length === element.childNodes.length)
            primitives.forEach((primitive, index) => {
                primitive.destroyCustomContent(<SVGElement>element.childNodes[index]);
            });

    }
    moveItemElements(parent: SVGElement, element: SVGElement, sibling?: SVGElement): void {
        if(element && parent)
            if(sibling)
                parent.insertBefore(element, sibling);
            else
                parent.appendChild(element);

    }
    changeItemChildrenVisibility(element: SVGElement, expanded: boolean): void {
        element.style.display = expanded ? "" : "none";
    }
    getItemGroupContainerKey(zIndex: number, parentContainerKey?: ItemKey): string {
        return parentContainerKey !== undefined ? zIndex + "_" + parentContainerKey : zIndex.toString();
    }
    getItemGroupContainer(zIndex: number, parentContainerKey?: ItemKey): SVGElement {
        const parent = parentContainerKey !== undefined ? this.getItemParent(zIndex, parentContainerKey) : this.itemsContainer;
        const key = parentContainerKey || "Main";
        if(this.itemGroupContainers[key] === undefined || this.itemGroupContainers[key][zIndex] === undefined) {
            if(this.itemGroupContainers[key] === undefined)
                this.itemGroupContainers[key] = [];
            const nextSiblingZIndex = Object.keys(this.itemGroupContainers[key]).map(z => +z).sort((a, b) => a - b).filter(z => z > zIndex)[0];
            this.itemGroupContainers[key][zIndex] = this.createAndChangePrimitiveElement(
                new GroupPrimitive([], null, zIndex), parent, this.itemGroupContainers[key][nextSiblingZIndex]
            );
        }
        return this.itemGroupContainers[key][zIndex];
    }
    getItemSelectorGroupContainer(zIndex: number, parentContainerKey?: ItemKey): SVGElement {
        const parent = parentContainerKey !== undefined ? this.getItemSelectorParent(zIndex, parentContainerKey) : this.itemSelectorsContainer;
        const key = parentContainerKey || "Main";
        if(this.itemSelectorGroupContainers[key] === undefined || this.itemSelectorGroupContainers[key][zIndex] === undefined) {
            if(this.itemSelectorGroupContainers[key] === undefined)
                this.itemSelectorGroupContainers[key] = [];
            const nextSiblingZIndex = Object.keys(this.itemSelectorGroupContainers[key]).map(z => +z).sort((a, b) => a - b).filter(z => z > zIndex)[0];
            this.itemSelectorGroupContainers[key][zIndex] = this.createAndChangePrimitiveElement(
                new GroupPrimitive([], null, zIndex), parent, this.itemSelectorGroupContainers[key][nextSiblingZIndex]
            );
        }
        return this.itemSelectorGroupContainers[key][zIndex];
    }
    getItemParent(zIndex: number, parentContainerKey?: ItemKey): SVGElement {
        return parentContainerKey !== undefined ?
            this.itemChildElements[parentContainerKey] :
            this.getItemGroupContainer(zIndex);
    }
    getItemSelectorParent(zIndex: number, parentContainerKey?: ItemKey): SVGElement {
        return parentContainerKey !== undefined ?
            this.itemChildElements[parentContainerKey] :
            this.getItemSelectorGroupContainer(zIndex);
    }
    notifySelectionChanged(selection: Selection): void {
        const newSelectedItems = ModelUtils.createSelectedItems(selection);
        const itemsToUpdate : DiagramItem[] = [];
        this.populateItems(itemsToUpdate, newSelectedItems, this.selectedItems);
        this.populateItems(itemsToUpdate, this.selectedItems, newSelectedItems);
        this.selectedItems = newSelectedItems;
        itemsToUpdate.forEach(
            item => {
                if(item instanceof Connector && !this.pendingRemoveChanges[item.key])
                    this.applyOrPostponeChanges([new ItemChange(item, ItemChangeType.UpdateClassName, true)]);
            }
        );
    }
    private populateItems(target : DiagramItem[],
        sourceSet: { [key: string] : DiagramItem },
        checkSet: { [key: string] : DiagramItem }) : void {
        Object.keys(sourceSet).forEach(key => {
            if(!checkSet[key])
                target.push(sourceSet[key]);
        });
    }
}

export interface ILayoutPointResolver {
    getModelPoint(absolutePoint: Point, checkScrollArea?: boolean): Point;
    getAbsolutePoint(modelPoint: Point, excludeScroll?: boolean, checkScrollArea?: boolean): Point;
}
