import * as Arrays from "@paperbits/common/arrays";
import * as Utils from "@paperbits/common/utils";
import * as Html from "@paperbits/common/html";
import * as Objects from "@paperbits/common/objects";
import { ViewManager, ViewManagerMode, IHighlightConfig, IContextCommandSet, ActiveElement, View, IContextCommand } from "@paperbits/common/ui";
import { switchToParentCommand, defaultCommandColor, deleteWidgetCommand, splitter } from "@paperbits/common/ui/commands";
import { IWidgetBinding, WidgetContext, GridItem, GridHelper } from "@paperbits/common/editing";
import { IWidgetService } from "@paperbits/common/widgets";
import { EventManager, Events } from "@paperbits/common/events";
import { Router } from "@paperbits/common/routing";
import { Bag, Keys, MouseButtons } from "@paperbits/common";
import { LocalStyles, PluginBag, StyleDefinition, VariationContract } from "@paperbits/common/styles";
import { StyleHelper } from "@paperbits/styles";
import { TextblockEditor } from "../../textblock/ko";
import { ComponentFlow } from "@paperbits/common/components";



const contentEditorElementId = "contentEditor";

export class GridEditor {
    private activeHighlightedGridItem: GridItem;
    private scrolling: boolean;
    private scrollTimeout: any;
    private pointerX: number;
    private pointerY: number;
    private activeContextualCommands: IContextCommandSet;
    private activeElements: Bag<ActiveElement>;
    private ownerDocument: Document;
    private selection: GridItem;
    private activeLayer: string;
    private notificationTimeout: any;

    constructor(
        private readonly viewManager: ViewManager,
        private readonly widgetService: IWidgetService,
        private readonly eventManager: EventManager,
        private readonly router: Router
    ) {
        this.renderContextualCommands = this.renderContextualCommands.bind(this);
        this.onPointerDown = this.onPointerDown.bind(this);
        this.initialize = this.initialize.bind(this);
        this.dispose = this.dispose.bind(this);
        this.onDelete = this.onDelete.bind(this);
        this.onPointerMove = this.onPointerMove.bind(this);
        this.onWindowScroll = this.onWindowScroll.bind(this);
        this.onKeyDown = this.onKeyDown.bind(this);

        this.activeElements = {};
    }

    private isModelBeingEdited(gridItem: GridItem): boolean {
        const editorView = this.viewManager.getActiveView();

        if (!editorView) {
            return false;
        }

        let editorComponentName: string;

        if (typeof gridItem.editor === "string") {
            editorComponentName = gridItem.editor;
        }
        else {
            const registration = Reflect.getMetadata("paperbits-component", gridItem.editor);
            editorComponentName = registration.name;
        }

        if (editorView.component.name !== editorComponentName) {
            return false;
        }

        return true;
    }

    private isSelected(gridItem: GridItem): boolean {
        const selectedElement = this.viewManager.getSelectedElement();
        return selectedElement?.element === gridItem.element;
    }

    private getContextCommands(gridItem: GridItem, half: string = null): IContextCommandSet {
        const bindings = GridHelper.getParentWidgetBindings(gridItem.element);

        const providers = bindings
            .filter(x => !!x.provides)
            .map(x => x.provides)
            .reduce((acc, val) => acc.concat(val), []);

        const model = gridItem.binding.model;
        const binding = gridItem.binding;

        let parentModel;
        const parent = gridItem.getParent();
        const parentBinding = parent?.binding;

        if (parentBinding) {
            parentModel = parentBinding.model;
        }

        const context: WidgetContext = {
            gridItem: gridItem,
            parentModel: parentModel,
            parentBinding: parentBinding,
            model: model,
            binding: binding,
            half: half,
            providers: providers,
            openHelpCenter: (articleKey: string) => {
                const view: View = {
                    heading: "Help center",
                    component: {
                        name: "help-center",
                        params: {
                            articleKey: articleKey,
                        }
                    },
                    resizing: {
                        directions: "vertically horizontally",
                        initialWidth: 500,
                        initialHeight: 700
                    },
                    scrolling: false
                };

                this.viewManager.openViewAsPopup(view);
            },
            openWidgetEditor: () => {
                this.viewManager.openWidgetEditor(binding);
            },
            switchToParent: () => {
                const selectableParent = gridItem.getParent(null, true);

                if (!selectableParent) {
                    return;
                }

                const contextualCommands = selectableParent.getContextCommands();

                if (!contextualCommands) {
                    return;
                }

                this.selectElement(selectableParent);
            },
            switchToChild: () => {
                const children = gridItem.getChildren(this.activeLayer, true);

                if (children.length == 0) {
                    return;
                }

                const firstChild = children[0];
                this.selectElement(firstChild, true);
            },
            deleteWidget: () => {
                parentModel.widgets.remove(model);
                parentBinding.applyChanges();
                this.viewManager.clearContextualCommands();
            }
        };

        let contextualCommands: IContextCommandSet;
        let widgetHandler = this.widgetService.getWidgetHandler(binding);

        if (widgetHandler) {
            if (widgetHandler.getContextCommands) {
                try {
                    contextualCommands = widgetHandler.getContextCommands(context);
                }
                catch (error) {
                    console.warn(`Could not get context commands.`);
                }
            }
        }

        const defaultCommands = this.getDefaultContextCommands(context);

        if (!contextualCommands) {
            contextualCommands = defaultCommands;
        }

        const defaultCommand: IContextCommand = {
            controlType: "toolbox-button",
            callback: () => {
                if (!context.binding) {
                    return;
                }

                this.viewManager.openWidgetEditor(context.binding);
            }
        };

        contextualCommands.element = gridItem.element;
        contextualCommands.defaultCommand = defaultCommand;

        contextualCommands.selectCommands = contextualCommands.selectCommands !== undefined
            ? contextualCommands.selectCommands
            : defaultCommands?.selectCommands;

        contextualCommands.hoverCommands = contextualCommands.hoverCommands !== undefined
            ? contextualCommands.hoverCommands
            : defaultCommands?.hoverCommands;

        contextualCommands.deleteCommand = contextualCommands.deleteCommand !== undefined
            ? contextualCommands.deleteCommand
            : defaultCommands?.deleteCommand;

        return contextualCommands;
    }

    private onMouseClick(event: MouseEvent): void {
        const htmlElement = <HTMLElement>event.target;
        const htmlLinkElement = <HTMLLinkElement>htmlElement.closest("A");

        if (htmlLinkElement) {
            event.preventDefault(); // prevent default event handling for hyperlink controls
        }
    }

    private onPointerMove(event: PointerEvent): void {
        if (this.viewManager.mode === ViewManagerMode.pause) {
            event.preventDefault();
            event.stopPropagation();
            return;
        }

        this.pointerX = event.clientX;
        this.pointerY = event.clientY;

        switch (this.viewManager.mode) {
            case ViewManagerMode.selecting:
            case ViewManagerMode.selected:
            case ViewManagerMode.configure:
                this.renderHighlightedElements();
                break;

            case ViewManagerMode.dragging:
                this.renderDropHandlers();
                break;
        }
    }

    private onPointerDown(event: PointerEvent): void {
        if (event.ctrlKey || event.metaKey || this.viewManager.mode === ViewManagerMode.preview) {
            const htmlElement = <HTMLElement>event.target;
            const htmlLinkElement = <HTMLLinkElement>htmlElement.closest("A");

            if (!htmlLinkElement || htmlLinkElement.href.endsWith("#")) {
                return;
            }

            event.preventDefault();

            this.router.navigateTo(htmlLinkElement.href);
            return;
        }

        if (this.viewManager.mode === ViewManagerMode.pause) {
            event.preventDefault();
            event.stopPropagation();
            return;
        }

        if (event.button !== MouseButtons.Main) {
            return;
        }

        if (this.viewManager.mode !== ViewManagerMode.selecting &&
            this.viewManager.mode !== ViewManagerMode.selected &&
            this.viewManager.mode !== ViewManagerMode.configure) {
            return;
        }

        const gridItems = this.getGridItemsUnderPointer();

        if (gridItems.length === 0) {
            return;
        }

        const topGridItem = gridItems[0];

        if (topGridItem.binding.readonly) {
            return;
        }

        if (topGridItem.binding.layer !== "*" && topGridItem.binding.layer !== this.activeLayer) {
            event.preventDefault();
            event.stopPropagation();
            this.eventManager.dispatchEvent("displayInactiveLayoutHint", topGridItem.binding.model);
            return;
        }

        if (gridItems.length === 1 && topGridItem.binding.name === "content" && topGridItem.binding.layer === this.activeLayer) {
            this.viewManager.clearContextualCommands();
            return;
        }

        const gridItem = topGridItem;

        if (!gridItem) {
            return;
        }

        if (gridItem.editor !== TextblockEditor) {
            /**
             * Special case for text editor. All other widget element (like a hyperlink or a form submit)
             * should not trigger their default events.
             */
            event.preventDefault();
        }

        if (this.isModelBeingEdited(gridItem)) {
            return;
        }

        if (this.isSelected(gridItem)) {
            const commandSet = gridItem.getContextCommands();

            if (commandSet.defaultCommand) {
                commandSet.defaultCommand.callback();
            }
        }
        else {
            event.preventDefault(); // To prevent document selection.

            if (gridItem.element["dragSource"]) { // TODO: Maybe make part of Binding?
                gridItem.element["dragSource"].beginDrag(gridItem.element, this.pointerX, this.pointerY);
            }

            this.selectElement(gridItem);
        }
    }

    private onKeyDown(event: KeyboardEvent): void {
        if (document.activeElement.id !== contentEditorElementId) {
            return;
        }

        const selectedElement = this.viewManager.getSelectedElement();

        if (!selectedElement) {
            return;
        }

        const gridItem = this.getGridItem(selectedElement.element);

        if (!gridItem) {
            return;
        }

        switch (event.key) {
            case Keys.ArrowDown:
            case Keys.ArrowRight:
                const next = gridItem.getNextSibling();

                if (next) {
                    this.selectElement(next, true);
                }

                break;

            case Keys.ArrowUp:
            case Keys.ArrowLeft:
                const prev = gridItem.getPrevSibling();

                if (prev) {
                    this.selectElement(prev, true);
                }

                break;

            case Keys.PageUp:
                const parent = gridItem.getParent(this.activeLayer, true);

                if (parent) {
                    this.selectElement(parent, true);
                }
                break;

            case Keys.PageDown:
                const children = gridItem.getChildren(this.activeLayer, true);

                if (children.length > 0) {
                    const firstChild = children[0];
                    this.selectElement(firstChild, true);
                }
                break;

            case Keys.Enter:
                // if (gridItem.binding.editor) {
                //     this.viewManager.openWidgetEditor(gridItem.binding);
                // }
                break;

            case Keys.Escape:
                // On Esc we should return forcus to selected element
                break;

            default:
                return; // Ignore other keys
        }
    }

    private selectElement(item: GridItem, scrollIntoView: boolean = false): void {
        if (!item) {
            throw new Error(`Parameter "item" not specified.`);
        }

        this.eventManager.dispatchEvent(Events.NotificationRequest, item.displayName);

        if (this.notificationTimeout) {
            clearTimeout(this.notificationTimeout);
        }

        this.notificationTimeout = setTimeout(() => {
            this.eventManager.dispatchEvent(Events.NotificationRequest, "selected");
        }, 1000);


        const commandSet = item?.getContextCommands();

        if (!commandSet) {
            return;
        }

        const config: IHighlightConfig = {
            element: item.element,
            text: item.displayName,
            color: commandSet.color
        };

        this.viewManager.setSelectedElement(config, commandSet);
        this.activeContextualCommands = commandSet;
        this.selection = item;
        document.getElementById(contentEditorElementId).focus();

        if (scrollIntoView) {
            item.element.scrollIntoView({ behavior: "smooth", block: "nearest" });
        }
    }

    private renderDropHandlers(): void {
        const dragSession = this.viewManager.getDragSession();

        if (!dragSession) {
            return;
        }

        const stack = this.getGridItemsUnderPointer();

        if (stack.length === 0) {
            return;
        }

        const acceptingParent = stack.find(x => {
            if (!x.binding) {
                return false;
            }

            const handler = this.widgetService.getWidgetHandler(x.binding);

            if (handler?.canAccept && handler.canAccept(dragSession)) {
                return true;
            }

            return false;
        });

        if (!acceptingParent) {
            delete dragSession.targetElement;
            delete dragSession.targetBinding;

            this.viewManager.setSplitter(null);
            return;
        }

        dragSession.targetElement = acceptingParent.element;
        dragSession.targetBinding = acceptingParent.binding;

        const siblingElement: GridItem = stack.find(x => x.element.parentElement === acceptingParent.element);

        if (siblingElement) {
            const quadrant = Utils.pointerToClientQuadrant(this.pointerX, this.pointerY, siblingElement.element);
            const sourceElementFlow = dragSession.sourceBinding.flow || ComponentFlow.Inline;

            dragSession.insertIndex = acceptingParent.binding.model.widgets.indexOf(siblingElement.binding.model);
            const hoveredElementFlow = siblingElement.binding.flow || ComponentFlow.Inline;

            if (sourceElementFlow === ComponentFlow.Inline && hoveredElementFlow === ComponentFlow.Inline) {
                if (quadrant.horizontal === "right") {
                    dragSession.insertIndex++;
                }

                this.viewManager.setSplitter({
                    element: siblingElement.element,
                    side: quadrant.horizontal,
                    where: "outside"
                });
            }
            else {
                if (quadrant.vertical === "bottom") {
                    dragSession.insertIndex++;
                }

                this.viewManager.setSplitter({
                    element: siblingElement.element,
                    side: quadrant.vertical,
                    where: "outside"
                });
            }
        }
        else {
            const quadrant = Utils.pointerToClientQuadrant(this.pointerX, this.pointerY, acceptingParent.element);

            if (acceptingParent.binding.model.widgets.length === 0) {
                dragSession.insertIndex = 0;

                this.viewManager.setSplitter({
                    element: acceptingParent.element,
                    side: quadrant.vertical,
                    where: "inside"
                });

                return;
            }
            else {
                const children = Array.prototype.slice.call(acceptingParent.element.children);

                if (quadrant.vertical === "top") {
                    dragSession.insertIndex = 0;

                    const child = children[0];

                    this.viewManager.setSplitter({
                        element: child,
                        side: "top",
                        where: "outside"
                    });
                }
                else {
                    dragSession.insertIndex = children.length;
                    const child = children[dragSession.insertIndex];

                    this.viewManager.setSplitter({
                        element: child,
                        side: "bottom",
                        where: "outside"
                    });
                }
            }
        }
    }

    private onDelete(): void {
        if (this.viewManager.mode === ViewManagerMode.selected && this.activeContextualCommands && this.activeContextualCommands.deleteCommand) {
            this.activeContextualCommands.deleteCommand.callback();
        }
    }

    private onWindowScroll(): void {
        if (this.viewManager.mode === ViewManagerMode.dragging ||
            this.viewManager.mode === ViewManagerMode.pause ||
            this.viewManager.mode === ViewManagerMode.configure) {
            return;
        }

        if (!this.scrolling) {
            this.viewManager.clearContextualCommands();
        }

        this.scrolling = true;

        if (this.scrollTimeout) {
            clearTimeout(this.scrollTimeout);
        }

        this.scrollTimeout = setTimeout(this.resetScrolling.bind(this), 100);
    }

    private resetScrolling(): void {
        this.scrolling = false;
        this.renderHighlightedElements();

        if (this.selection) { // also, check for element existence.
            this.selectElement(this.selection, false);
        }
    }

    private getElementsUnderPointer(): HTMLElement[] {
        const elements = Utils.elementsFromPoint(this.ownerDocument, this.pointerX, this.pointerY);

        // Determining cut-off index to exclude from selection stack the elements under popups or dropdowns.
        const cutoffIndex = elements.findIndex(element => getComputedStyle(element).zIndex !== "auto");

        if (cutoffIndex >= 0) {
            elements.splice(cutoffIndex + 1);
        }

        return elements;
    }

    private renderHighlightedElements(): void {
        if (this.scrolling) {
            return;
        }

        this.renderContextualCommands();
    }

    private getDefaultContextCommands(context: WidgetContext): IContextCommandSet {
        const contextCommands: IContextCommandSet = {
            color: defaultCommandColor,
            hoverCommands: [{
                controlType: "toolbox-button",
                color: defaultCommandColor,
                iconClass: "paperbits-icon paperbits-simple-add",
                position: context.half,
                tooltip: "Add widget",
                component: {
                    name: "widget-selector",
                    params: {
                        onRequest: () => context.providers,
                        onSelect: (newWidgetModel: any) => {
                            let index = context.parentModel.widgets.indexOf(context.model);

                            if (context.half === "bottom") {
                                index++;
                            }

                            context.parentBinding.model.widgets.splice(index, 0, newWidgetModel);
                            context.parentBinding.applyChanges();

                            this.viewManager.clearContextualCommands();
                        }
                    }
                }
            }],
            deleteCommand: deleteWidgetCommand(context),
            selectCommands: []
        };

        if (context.binding?.editor && context.binding?.applyChanges) {
            contextCommands.selectCommands.push({
                controlType: "toolbox-button",
                displayName: `Edit widget`,
                position: "top right",
                color: defaultCommandColor,
                callback: () => this.viewManager.openWidgetEditor(context.binding)
            });
            contextCommands.selectCommands.push(splitter());
        }

        contextCommands.selectCommands.push(switchToParentCommand(context));

        if (!context.half) { // Not selection mode.
            const handler = this.widgetService.getWidgetHandler(context.binding);

            if (!handler?.getStyleDefinitions) {
                return contextCommands;
            }

            const styleDefinitions = handler.getStyleDefinitions();

            if (!styleDefinitions?.components) {
                return contextCommands;
            }

            const styleEditorCommand = this.getStyleEditorCommand(context.gridItem.element, context.binding, styleDefinitions);

            if (styleEditorCommand) {
                contextCommands.selectCommands.push(styleEditorCommand);
            }
        }

        return contextCommands;
    }

    private renderContextualCommands(): void {
        let highlightedGridItem: GridItem;
        let highlightedText: string;
        let highlightColor: string;

        const tobeDeleted = Object.keys(this.activeElements);
        const gridItems = this.getGridItemsUnderPointer(this.activeLayer);

        tobeDeleted.forEach(key => {
            this.viewManager.removeContextualCommands(key);
            delete this.activeElements[key];
        });

        for (let i = gridItems.length - 1; i >= 0; i--) {
            const gridItem = gridItems[i];
            const index = tobeDeleted.indexOf(gridItem.name);
            tobeDeleted.splice(index, 1);

            const quadrant = Utils.pointerToClientQuadrant(this.pointerX, this.pointerY, gridItem.element);
            const half = quadrant.vertical;
            const activeElement = this.activeElements[gridItem.name];
            const contextualCommandSet = gridItem.getContextCommands(half);

            highlightedGridItem = gridItem;
            highlightedText = gridItem.displayName;
            highlightColor = contextualCommandSet.color;

            if (!activeElement || gridItem.element !== activeElement.element || half !== activeElement.half) {
                this.viewManager.setContextualCommands(gridItem.name, contextualCommandSet);

                this.activeElements[gridItem.name] = {
                    key: gridItem.name,
                    element: gridItem.element,
                    half: quadrant.vertical
                };
            }
        }

        const isTopLevelWidget = highlightedGridItem?.binding.name === "content" && this.activeLayer === highlightedGridItem?.binding.layer;

        if (!highlightedGridItem || isTopLevelWidget) {
            this.activeHighlightedGridItem = null;
            this.viewManager.setHighlight(null);
            return;
        }

        if (this.activeHighlightedGridItem !== highlightedGridItem && !highlightedGridItem?.binding?.readonly) {
            this.activeHighlightedGridItem = highlightedGridItem;

            this.viewManager.setHighlight({
                element: highlightedGridItem?.element,
                text: highlightedText, color: highlightColor
            });
        }
    }

    private findFocusableElement(): GridItem {
        const element = <HTMLElement>Html.findFirst(this.ownerDocument.body,
            node => node.nodeName === "SECTION" && !!this.getGridItem(<HTMLElement>node));

        if (!element) {
            return null;
        }

        return this.getGridItem(element);
    }

    private onGlobalFocusChange(event: FocusEvent): void {
        const target = <HTMLElement>event.target;

        if (target.id === contentEditorElementId) {
            if (this.selection) { // also, check for element existence.
                this.selectElement(this.selection, false);
            }
            else {
                const focusableElement = this.findFocusableElement();

                if (focusableElement) {
                    this.selectElement(focusableElement);
                }
            }
        }
        else {
            if (this.selection) {
                const toolbox = target.closest(".toolbox");

                if (toolbox && !toolbox.classList.contains("toolbox-context")) {
                    this.viewManager.clearSelection();
                }
            }
        }
    }

    public initialize(ownerDocument: Document): void {
        this.activeLayer = this.viewManager.getActiveLayer();
        this.ownerDocument = ownerDocument;
        // Firefox doesn't fire "pointermove" events by some reason
        this.ownerDocument.addEventListener(Events.MouseMove, this.onPointerMove, true);
        this.ownerDocument.addEventListener(Events.Scroll, this.onWindowScroll);
        this.ownerDocument.addEventListener(Events.MouseDown, this.onPointerDown, true);
        this.ownerDocument.addEventListener(Events.Click, this.onMouseClick, true);
        this.eventManager.addEventListener("onKeyDown", this.onKeyDown);
        this.eventManager.addEventListener("onDelete", this.onDelete);

        document.addEventListener(Events.Focus, (e) => this.onGlobalFocusChange(e), true);

        // Prevent zoom on the content
        ownerDocument.addEventListener("wheel", (event: WheelEvent): void => {
            if (event.ctrlKey) {
                event.preventDefault();
            }

            const scrollTarget = <HTMLElement>event.target;
            scrollTarget.scrollBy(event.deltaX, event.deltaY);
        }, { passive: false });
    }

    public dispose(): void {
        this.ownerDocument.removeEventListener(Events.MouseMove, this.onPointerMove, true);
        this.ownerDocument.removeEventListener(Events.Scroll, this.onWindowScroll);
        this.ownerDocument.removeEventListener(Events.MouseDown, this.onPointerDown, true);
        this.ownerDocument.removeEventListener(Events.Click, this.onMouseClick, false);
        this.eventManager.removeEventListener("onKeyDown", this.onKeyDown);
        this.eventManager.removeEventListener("onDelete", this.onDelete);
    }

    private getChildren(gridItem: GridItem, layerName?: string): GridItem[] {
        const childElements = Arrays.coerce<HTMLElement>(gridItem.element.querySelectorAll("*"));

        return childElements
            .map(child => this.getGridItem(child, layerName))
            .filter(x => !!x && x.binding.model !== gridItem.binding.model && !x.binding.readonly);
    }

    private getParent(gridItem: GridItem, layerName?: string, excludeReadonly?: boolean): GridItem {
        const elements = Html.parents(gridItem.element);
        const parentGridItems = this.getGridItems(elements, layerName);

        return excludeReadonly
            ? parentGridItems.find(x => !x.binding.readonly)
            : parentGridItems.find(() => true);
    }

    private getSiblings(gridItem: GridItem): GridItem[] {
        const parent = this.getParent(gridItem);
        return this.getChildren(parent);
    }

    private getNextSibling(gridItem: GridItem): GridItem {
        const nextElement = <HTMLElement>gridItem.element.nextElementSibling;

        if (!nextElement) {
            return null;
        }

        return this.getGridItem(nextElement);
    }

    private getPrevSibling(gridItem: GridItem): GridItem {
        const previousElement = <HTMLElement>gridItem.element.previousElementSibling;

        if (!previousElement) {
            return null;
        }

        return this.getGridItem(previousElement);
    }

    private getGridItemsUnderPointer(layerName?: string): GridItem[] {
        const elements = this.getElementsUnderPointer().filter(x => x.tagName !== "HTML");

        return this.getGridItems(elements, layerName);
    }

    private getGridItem(element: HTMLElement, requestedLayerName?: string): GridItem {
        const widgetBinding = GridHelper.getWidgetBinding(element);

        if (!widgetBinding) {
            return null;
        }

        const isAnotherLayer = widgetBinding.layer !== "*" && requestedLayerName && widgetBinding.layer !== requestedLayerName;

        if (isAnotherLayer) {
            return null;
        }

        const gridItem: GridItem = {
            name: widgetBinding.name,
            displayName: widgetBinding.displayName,
            editor: widgetBinding.editor,
            element: element,
            binding: widgetBinding,
            getParent: (layerName: string, excludeReadonly: boolean) => this.getParent(gridItem, layerName, excludeReadonly),
            getChildren: (layerName: string) => this.getChildren(gridItem, layerName),
            getSiblings: () => this.getSiblings(gridItem),
            getNextSibling: () => this.getNextSibling(gridItem),
            getPrevSibling: () => this.getPrevSibling(gridItem),
            getContextCommands: (half) => this.getContextCommands(gridItem, half),
            select: (scrollIntoView: boolean = false) => this.selectElement(gridItem, scrollIntoView)
        };

        return gridItem;
    }

    private getStyleEditorCommand(element: HTMLElement, binding: IWidgetBinding<any, any>, styleDefinitions: StyleDefinition): IContextCommand {
        if (!styleDefinitions.components) {
            return null;
        }

        const componentStyleDefinitionWrapper = StyleHelper.getStyleDefinitionWrappers(styleDefinitions.components);
        const match = componentStyleDefinitionWrapper.find(x => element.matches(x.selector));

        if (!match) {
            return null;
        }

        const componentStyleDefinition = match.definition;
        const styleKeySegments = match.key.split("/");

        const styleEditorCommand: IContextCommand = {
            controlType: "toolbox-button",
            name: "edit",
            tooltip: "Edit styles",
            iconClass: "paperbits-icon paperbits-palette",
            position: "top right",
            color: "#607d8b",
            callback: () => {
                const styles: LocalStyles = binding.model?.styles;

                let shortKey: string;

                if (styleKeySegments.length > 2) { // TODO: simplify
                    shortKey = "instance/" + styleKeySegments.slice(2).join("/");
                }
                else {
                    shortKey = "instance";
                }

                let componentVariation: PluginBag;
                componentVariation = Objects.getObjectAt(shortKey, styles);

                if (!componentVariation) {
                    componentVariation = {
                        key: null, // key will be generated by style editor.
                        ...componentStyleDefinition.defaults
                    };

                    Objects.setValue(shortKey, styles, componentVariation);
                }

                const view: View = {
                    heading: `${componentStyleDefinition.displayName} styles`,
                    component: {
                        name: "style-editor",
                        params: {
                            elementStyle: componentVariation,
                            baseComponentKey: componentStyleDefinition.baseComponentKey,
                            plugins: componentStyleDefinition.plugins,
                            onUpdate: (): void => {
                                binding.applyChanges(binding.model);
                            }
                        }
                    },
                    resizing: "vertically horizontally"
                };

                this.viewManager.openViewAsPopup(view);
            }
        };

        return styleEditorCommand;
    }

    private getStylableGridItem(element: HTMLElement): GridItem {
        const binding = GridHelper.getClosestParentBinding(element);

        if (!binding) {
            return null;
        }

        const handler = this.widgetService.getWidgetHandler(binding);

        if (!handler?.getStyleDefinitions) {
            return null;
        }

        const styleDefinitions = handler.getStyleDefinitions();

        if (!styleDefinitions?.components) {
            return null;
        }

        const componentStyleDefinitionWrapper = StyleHelper.getStyleDefinitionWrappers(styleDefinitions.components);
        const match = componentStyleDefinitionWrapper.find(x => element.matches(x.selector));

        if (!match) {
            return null;
        }

        const selectCommands: IContextCommand[] = [];

        const styleEditorCommand = this.getStyleEditorCommand(element, binding, styleDefinitions);

        if (styleEditorCommand) {
            selectCommands.push(styleEditorCommand);
        }

        const componentStyleDefinition = match.definition;

        const gridItem: GridItem = {
            name: "style",
            displayName: componentStyleDefinition.displayName,
            binding: binding,
            element: element,
            editor: "style-editor",
            getParent: () => this.getParent(gridItem),
            getChildren: () => this.getChildren(gridItem),
            getSiblings: () => this.getSiblings(gridItem),
            getNextSibling: () => this.getNextSibling(gridItem),
            getPrevSibling: () => this.getPrevSibling(gridItem),
            getContextCommands: () => {
                const contextualCommands: IContextCommandSet = {
                    element: element,
                    defaultCommand: styleEditorCommand,
                    selectCommands: [styleEditorCommand],
                    hoverCommands: null,
                    deleteCommand: null
                };

                return contextualCommands;
            },
            select: (scrollIntoView: boolean = false) => this.selectElement(gridItem, scrollIntoView)
        };

        return gridItem;
    }

    private getGridItems(elements: HTMLElement[], layerName: string): GridItem[] {
        let currentwidgetBinding: IWidgetBinding<any, any>;
        const stackOfGridItems = [];

        for (const element of elements.reverse()) {
            const widgetGridItem = this.getGridItem(element, layerName);

            if (widgetGridItem && widgetGridItem.binding !== currentwidgetBinding) {
                stackOfGridItems.push(widgetGridItem);
                currentwidgetBinding = widgetGridItem.binding;
                continue;
            }

            const styleableGridItem = this.getStylableGridItem(element);

            if (styleableGridItem) {
                stackOfGridItems.push(styleableGridItem);
            }
        }

        return stackOfGridItems.reverse();
    }
}