/*******************************************************************************
 * Copyright (c) 2019, 2020 Dirk Fauth.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Dirk Fauth <dirk.fauth@googlemail.com> - initial API and implementation
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.group.performance;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.eclipse.collections.api.list.primitive.MutableIntList;
import org.eclipse.collections.impl.factory.primitive.IntLists;
import org.eclipse.nebula.widgets.nattable.NatTable;
import org.eclipse.nebula.widgets.nattable.command.ILayerCommand;
import org.eclipse.nebula.widgets.nattable.coordinate.PositionUtil;
import org.eclipse.nebula.widgets.nattable.freeze.CompositeFreezeLayer;
import org.eclipse.nebula.widgets.nattable.grid.GridRegion;
import org.eclipse.nebula.widgets.nattable.grid.layer.DimensionallyDependentIndexLayer;
import org.eclipse.nebula.widgets.nattable.grid.layer.DimensionallyDependentLayer;
import org.eclipse.nebula.widgets.nattable.group.ColumnGroupUtils;
import org.eclipse.nebula.widgets.nattable.group.command.ColumnGroupExpandCollapseCommand;
import org.eclipse.nebula.widgets.nattable.group.performance.GroupModel.Group;
import org.eclipse.nebula.widgets.nattable.group.performance.GroupModel.IndexPositionConverter;
import org.eclipse.nebula.widgets.nattable.group.performance.command.ColumnGroupCollapseCommand;
import org.eclipse.nebula.widgets.nattable.group.performance.command.ColumnGroupExpandCommand;
import org.eclipse.nebula.widgets.nattable.group.performance.command.ColumnGroupReorderCommandHandler;
import org.eclipse.nebula.widgets.nattable.group.performance.command.ColumnGroupReorderEndCommandHandler;
import org.eclipse.nebula.widgets.nattable.group.performance.command.ColumnGroupReorderStartCommandHandler;
import org.eclipse.nebula.widgets.nattable.group.performance.command.ColumnGroupsCommandHandler;
import org.eclipse.nebula.widgets.nattable.group.performance.command.GroupColumnReorderCommandHandler;
import org.eclipse.nebula.widgets.nattable.group.performance.command.GroupColumnReorderEndCommandHandler;
import org.eclipse.nebula.widgets.nattable.group.performance.command.GroupColumnReorderStartCommandHandler;
import org.eclipse.nebula.widgets.nattable.group.performance.command.GroupMultiColumnReorderCommand;
import org.eclipse.nebula.widgets.nattable.group.performance.command.GroupMultiColumnReorderCommandHandler;
import org.eclipse.nebula.widgets.nattable.group.performance.command.UpdateColumnGroupCollapseCommand;
import org.eclipse.nebula.widgets.nattable.group.performance.config.DefaultColumnGroupHeaderLayerConfiguration;
import org.eclipse.nebula.widgets.nattable.group.performance.config.GroupHeaderConfigLabels;
import org.eclipse.nebula.widgets.nattable.group.performance.painter.ColumnGroupHeaderGridLineCellLayerPainter;
import org.eclipse.nebula.widgets.nattable.layer.AbstractLayerTransform;
import org.eclipse.nebula.widgets.nattable.layer.DataLayer;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.layer.ILayerListener;
import org.eclipse.nebula.widgets.nattable.layer.IUniqueIndexLayer;
import org.eclipse.nebula.widgets.nattable.layer.LabelStack;
import org.eclipse.nebula.widgets.nattable.layer.LayerUtil;
import org.eclipse.nebula.widgets.nattable.layer.SizeConfig;
import org.eclipse.nebula.widgets.nattable.layer.cell.IConfigLabelProvider;
import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
import org.eclipse.nebula.widgets.nattable.layer.cell.LayerCell;
import org.eclipse.nebula.widgets.nattable.layer.cell.TransformedLayerCell;
import org.eclipse.nebula.widgets.nattable.layer.command.ConfigureScalingCommandHandler;
import org.eclipse.nebula.widgets.nattable.layer.event.ColumnStructuralChangeEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.ColumnStructuralRefreshEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.IStructuralChangeEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.RowStructuralRefreshEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.StructuralDiff;
import org.eclipse.nebula.widgets.nattable.layer.event.StructuralDiff.DiffTypeEnum;
import org.eclipse.nebula.widgets.nattable.painter.layer.ILayerPainter;
import org.eclipse.nebula.widgets.nattable.reorder.event.ColumnReorderEvent;
import org.eclipse.nebula.widgets.nattable.resize.command.MultiRowResizeCommand;
import org.eclipse.nebula.widgets.nattable.resize.command.RowResizeCommand;
import org.eclipse.nebula.widgets.nattable.resize.event.RowResizeEvent;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer.MoveDirectionEnum;
import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
import org.eclipse.nebula.widgets.nattable.util.ArrayUtil;
import org.eclipse.swt.graphics.Rectangle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Adds the column grouping functionality to the column header. Also persists
 * the state of the column groups when
 * {@link NatTable#saveState(String, Properties)} is invoked.
 * <p>
 * Internally uses a collection of {@link GroupModel} to track the column groups
 * on multiple levels.
 * </p>
 * <p>
 * It supports multiple column grouping levels. The levels are 0 based and
 * configured bottom up. That means if 3 levels of column groups are defined,
 * the first level==0 is the bottom most rowPosition==2, and the top most
 * level==2 is on rowPosition==0.
 * </p>
 *
 * @since 1.6
 */
public class ColumnGroupHeaderLayer extends AbstractLayerTransform {

    private static final Logger LOG = LoggerFactory.getLogger(ColumnGroupHeaderLayer.class);

    private static final String PERSISTENCE_KEY_COLUMN_GROUPS = ".columnGroups"; //$NON-NLS-1$

    private final List<GroupModel> model;

    /**
     * {@link SizeConfig} instance for the row height configuration.
     */
    private final SizeConfig rowHeightConfig = new SizeConfig(DataLayer.DEFAULT_ROW_HEIGHT);

    /**
     * Flag which is used to tell the {@link ColumnGroupHeaderLayer} whether to
     * calculate the height of the layer dependent on column group configuration
     * or not. If it is set to <code>true</code> the column header will check if
     * column groups are configured and if not, the height of the column header
     * will not show the double height for showing column groups.
     */
    private boolean calculateHeight = false;

    /**
     * Flag to configure whether group names should be always visible on
     * rendering, e.g. on scrolling, or if the group names should scroll with
     * the cell. Default is <code>false</code>.
     */
    private boolean showAlwaysGroupNames = false;

    /**
     * The layer to which the positions in the group should match. Typically
     * this is the {@link SelectionLayer}.
     */
    private IUniqueIndexLayer positionLayer;

    /**
     * The converter that is used to perform the index-position conversion in a
     * {@link GroupModel}.
     */
    private IndexPositionConverter indexPositionConverter;

    /**
     * The path of {@link ILayer} from {@link #positionLayer} to this layer.
     * Needed to be able to convert the column position based on the
     * {@link #positionLayer} to a position in this layer.
     */
    private List<ILayer> layerPath;

    /**
     * Position that is tracked on column group reorder via dragging. Needed
     * because on drag operations the viewport could scroll and therefore the
     * from position is not the original one anymore.
     */
    private int reorderFromColumnPosition;

    /**
     * Map in which it is stored if reordering is supported per level.
     */
    private Map<Integer, Boolean> reorderSupportedOnLevel = new HashMap<>();

    /**
     * The {@link CompositeFreezeLayer} in case it is part of the layer
     * composition. Needed to deal with groups in frozen state as column
     * positions could get ambiguous on scrolling.
     */
    private CompositeFreezeLayer compositeFreezeLayer;

    /**
     * Creates a {@link ColumnGroupHeaderLayer} with the specified
     * configurations and one grouping level. Uses the SelectionLayer as
     * positionLayer and the default configuration.
     *
     * @param underlyingHeaderLayer
     *            The underlying layer on whose top this layer should be
     *            created, typically the ColumnHeaderLayer.
     * @param selectionLayer
     *            The SelectionLayer needed for command handlers that inspect
     *            the selection on handling.
     */
    public ColumnGroupHeaderLayer(
            ILayer underlyingHeaderLayer,
            SelectionLayer selectionLayer) {

        this(underlyingHeaderLayer, selectionLayer, selectionLayer, 1, true);
    }

    /**
     * Creates a {@link ColumnGroupHeaderLayer} with the specified
     * configurations. Uses the SelectionLayer as positionLayer and the default
     * configuration.
     *
     * @param underlyingHeaderLayer
     *            The underlying layer on whose top this layer should be
     *            created, typically the ColumnHeaderLayer.
     * @param selectionLayer
     *            The SelectionLayer needed for command handlers that inspect
     *            the selection on handling.
     * @param numberOfGroupLevels
     *            The number of group levels that should be supported.
     *            Additional levels can also be added via
     *            {@link #addGroupingLevel()}.
     */
    public ColumnGroupHeaderLayer(
            ILayer underlyingHeaderLayer,
            SelectionLayer selectionLayer,
            int numberOfGroupLevels) {

        this(underlyingHeaderLayer, selectionLayer, selectionLayer, numberOfGroupLevels, true);
    }

    /**
     * Creates a {@link ColumnGroupHeaderLayer} with the specified
     * configurations and one grouping level. Uses the default configuration.
     *
     * @param underlyingHeaderLayer
     *            The underlying layer on whose top this layer should be
     *            created, typically the ColumnHeaderLayer.
     * @param positionLayer
     *            The positionLayer to which this layer should be mapped to,
     *            needed to handle column position transformations without
     *            taking the viewport into account. Typically the
     *            SelectionLayer.
     * @param selectionLayer
     *            The SelectionLayer needed for command handlers that inspect
     *            the selection on handling.
     */
    public ColumnGroupHeaderLayer(
            ILayer underlyingHeaderLayer,
            IUniqueIndexLayer positionLayer,
            SelectionLayer selectionLayer) {

        this(underlyingHeaderLayer, positionLayer, selectionLayer, 1, true);
    }

    /**
     * Creates a {@link ColumnGroupHeaderLayer} with the specified
     * configurations. Uses the default configuration.
     *
     * @param underlyingHeaderLayer
     *            The underlying layer on whose top this layer should be
     *            created, typically the ColumnHeaderLayer.
     * @param positionLayer
     *            The positionLayer to which this layer should be mapped to,
     *            needed to handle column position transformations without
     *            taking the viewport into account. Typically the
     *            SelectionLayer.
     * @param selectionLayer
     *            The SelectionLayer needed for command handlers that inspect
     *            the selection on handling.
     * @param numberOfGroupLevels
     *            The number of group levels that should be supported.
     *            Additional levels can also be added via
     *            {@link #addGroupingLevel()}.
     */
    public ColumnGroupHeaderLayer(
            ILayer underlyingHeaderLayer,
            IUniqueIndexLayer positionLayer,
            SelectionLayer selectionLayer,
            int numberOfGroupLevels) {

        this(underlyingHeaderLayer, positionLayer, selectionLayer, numberOfGroupLevels, true);
    }

    /**
     * Creates a {@link ColumnGroupHeaderLayer} with the specified
     * configurations. Takes the {@link SelectionLayer} as positionLayer.
     *
     * @param underlyingHeaderLayer
     *            The underlying layer on whose top this layer should be
     *            created, typically the ColumnHeaderLayer.
     * @param selectionLayer
     *            The SelectionLayer needed for command handlers that inspect
     *            the selection on handling.
     * @param numberOfGroupLevels
     *            The number of group levels that should be supported.
     *            Additional levels can also be added via
     *            {@link #addGroupingLevel()}.
     * @param useDefaultConfiguration
     *            <code>true</code> if the default configuration should be
     *            applied, <code>false</code> if a custom configuration will be
     *            applied afterwards.
     */
    public ColumnGroupHeaderLayer(
            ILayer underlyingHeaderLayer,
            SelectionLayer selectionLayer,
            int numberOfGroupLevels,
            boolean useDefaultConfiguration) {

        this(underlyingHeaderLayer, selectionLayer, selectionLayer, numberOfGroupLevels, useDefaultConfiguration);
    }

    /**
     * Creates a {@link ColumnGroupHeaderLayer} with one grouping level and the
     * specified configurations. Takes the {@link SelectionLayer} as
     * positionLayer.
     *
     * @param underlyingHeaderLayer
     *            The underlying layer on whose top this layer should be
     *            created, typically the ColumnHeaderLayer.
     * @param selectionLayer
     *            The SelectionLayer needed for command handlers that inspect
     *            the selection on handling.
     * @param useDefaultConfiguration
     *            <code>true</code> if the default configuration should be
     *            applied, <code>false</code> if a custom configuration will be
     *            applied afterwards.
     */
    public ColumnGroupHeaderLayer(
            ILayer underlyingHeaderLayer,
            SelectionLayer selectionLayer,
            boolean useDefaultConfiguration) {

        this(underlyingHeaderLayer, selectionLayer, selectionLayer, 1, useDefaultConfiguration);
    }

    /**
     * Creates a {@link ColumnGroupHeaderLayer} with one grouping level and the
     * specified configurations.
     *
     * @param underlyingHeaderLayer
     *            The underlying layer on whose top this layer should be
     *            created, typically the ColumnHeaderLayer.
     * @param positionLayer
     *            The positionLayer to which this layer should be mapped to,
     *            needed to handle column position transformations without
     *            taking the viewport into account. Typically the
     *            SelectionLayer.
     * @param selectionLayer
     *            The SelectionLayer needed for command handlers that inspect
     *            the selection on handling.
     * @param useDefaultConfiguration
     *            <code>true</code> if the default configuration should be
     *            applied, <code>false</code> if a custom configuration will be
     *            applied afterwards.
     */
    public ColumnGroupHeaderLayer(
            ILayer underlyingHeaderLayer,
            IUniqueIndexLayer positionLayer,
            SelectionLayer selectionLayer,
            boolean useDefaultConfiguration) {
        this(underlyingHeaderLayer, selectionLayer, selectionLayer, 1, useDefaultConfiguration);
    }

    /**
     * Creates a {@link ColumnGroupHeaderLayer} with the specified
     * configurations.
     *
     * @param underlyingHeaderLayer
     *            The underlying layer on whose top this layer should be
     *            created, typically the ColumnHeaderLayer.
     * @param positionLayer
     *            The positionLayer to which this layer should be mapped to,
     *            needed to handle column position transformations without
     *            taking the viewport into account. Typically the
     *            SelectionLayer.
     * @param selectionLayer
     *            The SelectionLayer needed for command handlers that inspect
     *            the selection on handling.
     * @param numberOfGroupLevels
     *            The number of group levels that should be supported.
     *            Additional levels can also be added via
     *            {@link #addGroupingLevel()}.
     * @param useDefaultConfiguration
     *            <code>true</code> if the default configuration should be
     *            applied, <code>false</code> if a custom configuration will be
     *            applied afterwards.
     */
    public ColumnGroupHeaderLayer(
            ILayer underlyingHeaderLayer,
            IUniqueIndexLayer positionLayer,
            SelectionLayer selectionLayer,
            int numberOfGroupLevels,
            boolean useDefaultConfiguration) {

        super(underlyingHeaderLayer);

        this.positionLayer = positionLayer;
        this.indexPositionConverter = new GroupModel.IndexPositionConverter() {

            @Override
            public int convertPositionToIndex(int position) {
                return positionLayer.getColumnIndexByPosition(position);
            }

            @Override
            public int convertIndexToPosition(int index) {
                return positionLayer.getColumnPositionByIndex(index);
            }
        };

        this.model = new ArrayList<>(numberOfGroupLevels);
        for (int i = 0; i < numberOfGroupLevels; i++) {
            GroupModel groupModel = new GroupModel();
            groupModel.setIndexPositionConverter(this.indexPositionConverter);
            this.model.add(groupModel);
            this.reorderSupportedOnLevel.put(i, Boolean.TRUE);
        }

        this.layerPath = findLayerPath(this, 0);
        this.layerPainter = new ColumnGroupHeaderGridLineCellLayerPainter(this);

        // add listener on dependent layer to be notified about structural
        // changes
        positionLayer.addLayerListener(new StructuralChangeLayerListener());

        registerCommandHandlers(selectionLayer);

        if (useDefaultConfiguration) {
            addConfiguration(new DefaultColumnGroupHeaderLayerConfiguration(false));
        }
    }

    /**
     * @return The {@link ILayerPainter} that is used by this layer. Typically
     *         the {@link ColumnGroupHeaderGridLineCellLayerPainter} to support
     *         rendering of huge column group cells by inspecting the
     *         {@link #showAlwaysGroupNames} attribute.
     */
    @Override
    public ILayerPainter getLayerPainter() {
        // return the ILayerPainter set to this layer, not the ILayerPainter
        // from the underlying layer as specified in AbstractLayerTransform
        return this.layerPainter;
    }

    @Override
    public void setLayerPainter(ILayerPainter layerPainter) {
        this.layerPainter = layerPainter;
    }

    /**
     * Register command handlers for this layer.
     *
     * @param selectionLayer
     *            The {@link SelectionLayer} needed for handling selections on
     *            grouping/ungrouping.
     */
    protected void registerCommandHandlers(SelectionLayer selectionLayer) {
        registerCommandHandler(new ColumnGroupsCommandHandler(this, selectionLayer));
        registerCommandHandler(new ConfigureScalingCommandHandler(null, this.rowHeightConfig));

        // group reordering
        registerCommandHandler(new ColumnGroupReorderCommandHandler(this));
        registerCommandHandler(new ColumnGroupReorderStartCommandHandler(this));
        registerCommandHandler(new ColumnGroupReorderEndCommandHandler(this));

        // register command handlers to add checks if a reordering is valid in
        // case of unbreakable groups
        getPositionLayer().registerCommandHandler(new GroupColumnReorderCommandHandler(this));
        getPositionLayer().registerCommandHandler(new GroupColumnReorderStartCommandHandler(this));
        getPositionLayer().registerCommandHandler(new GroupColumnReorderEndCommandHandler(this));
        getPositionLayer().registerCommandHandler(new GroupMultiColumnReorderCommandHandler(this));
    }

    /**
     * Convenience method to get the {@link GroupModel} on level 0. Useful for
     * single level column grouping.
     *
     * @return The {@link GroupModel} for level 0.
     */
    public GroupModel getGroupModel() {
        return getGroupModel(0);
    }

    /**
     * Return the {@link GroupModel} for the given grouping level. Note that the
     * levels are bottom up, so level 0 is the bottom most grouping level.
     *
     * @param level
     *            The grouping level. Value is bottom up.
     * @return The {@link GroupModel} for the corresponding level.
     */
    public GroupModel getGroupModel(int level) {
        if (level >= this.model.size()) {
            LOG.warn("tried to add a group on a non-existent level"); //$NON-NLS-1$
            return null;
        }
        return this.model.get(level);
    }

    /**
     * Adds a new grouping level on top.
     */
    public void addGroupingLevel() {
        GroupModel groupModel = new GroupModel();
        groupModel.setIndexPositionConverter(this.indexPositionConverter);
        this.model.add(groupModel);
        this.reorderSupportedOnLevel.put(this.model.size() - 1, Boolean.TRUE);
    }

    /**
     *
     * @return The number of grouping levels configured in this layer.
     */
    public int getLevelCount() {
        return this.model.size();
    }

    /**
     *
     * @return The layer to which the positions in the group should match.
     *         Typically this is the {@link SelectionLayer}.
     */
    public IUniqueIndexLayer getPositionLayer() {
        return this.positionLayer;
    }

    /**
     * Calculates the path of {@link ILayer} from {@link #positionLayer} to the
     * given layer.
     *
     * @param layer
     *            The {@link ILayer} for which the path is requested.
     * @param columnPosition
     *            The column position for which the layer path should be
     *            calculated.
     * @return The path of {@link ILayer} from the {@link #positionLayer} to the
     *         given {@link ILayer} or <code>null</code> if a direct path is not
     *         available.
     */
    List<ILayer> findLayerPath(ILayer layer, int columnPosition) {

        if (layer == getPositionLayer()) {
            List<ILayer> result = new ArrayList<>();
            result.add(layer);
            return result;
        }

        if (this.compositeFreezeLayer == null && layer instanceof CompositeFreezeLayer) {
            this.compositeFreezeLayer = (CompositeFreezeLayer) layer;
        }

        // handle collection
        List<ILayer> result = null;
        Collection<ILayer> underlyingLayers = layer.getUnderlyingLayersByColumnPosition(columnPosition);
        if (underlyingLayers != null) {
            for (ILayer underlyingLayer : underlyingLayers) {
                if (underlyingLayer != null) {
                    result = findLayerPath(underlyingLayer, columnPosition);
                }
            }
        }

        // handle horizontal dependency
        if (result == null && layer instanceof DimensionallyDependentLayer) {
            result = findLayerPath(((DimensionallyDependentLayer) layer).getHorizontalLayerDependency(), columnPosition);
        }
        if (result == null && this.underlyingLayer instanceof DimensionallyDependentIndexLayer) {
            result = findLayerPath(((DimensionallyDependentIndexLayer) layer).getHorizontalLayerDependency(), columnPosition);
        }

        // in case of the CompositeFreezeLayer it can happen that for the last
        // columns in scrolled state the path cannot be determined as
        // getUnderlyingLayersByPosition() returns an empty collection because
        // it is above the ViewportLayer. We therefore need a special handling
        // to check additionally below the ViewportLayer.
        if (result == null && layer instanceof CompositeFreezeLayer) {
            result = findLayerPath(((CompositeFreezeLayer) layer).getChildLayerByLayoutCoordinate(1, 1), columnPosition);
        }

        if (result != null) {
            result.add(layer);
        }

        return result;
    }

    /**
     * Converts the given column position the {@link #layerPath} upwards.
     *
     * @param columnPosition
     *            The column position to convert.
     * @return The upwards converted column position.
     */
    protected int convertColumnPositionUpwards(int columnPosition) {
        int converted = columnPosition;

        // This could be for example when the CompositeFreezeLayer is in the
        // composition. At creation time the underlying layers would be empty
        // because the width is not yet calculated.
        List<ILayer> path = this.layerPath;
        if (path == null) {
            path = findLayerPath(this, columnPosition);
        }

        if (path != null) {
            for (int i = 0; i < path.size() - 1; i++) {
                ILayer underlying = path.get(i);
                ILayer upper = path.get(i + 1);
                converted = upper.underlyingToLocalColumnPosition(underlying, converted);
            }
        }
        return converted;
    }

    @Override
    public boolean doCommand(ILayerCommand command) {
        if (command instanceof ColumnGroupExpandCollapseCommand
                && command.convertToTargetLayer(getPositionLayer())) {
            // only ColumnGroupExpandCollapseCommand needs to be converted to
            // positionLayer so also currently not visible column groups can be
            // expanded/collapsed, e.g. via ColumnChooser
            ColumnGroupExpandCollapseCommand cmd = (ColumnGroupExpandCollapseCommand) command;
            int rowPosition = cmd.getLocalRowPosition(this);
            int columnPosition = cmd.getColumnPosition();

            Object[] found = findGroupForCoordinates(columnPosition, rowPosition);
            if (found != null) {
                GroupModel groupModel = (GroupModel) found[0];
                Group group = (Group) found[1];
                if (group.isCollapsed()) {
                    expandGroup(groupModel, group);
                } else {
                    collapseGroup(groupModel, group);
                }
            }
            return true;
        } else if (command instanceof RowResizeCommand
                && command.convertToTargetLayer(this)
                && ((RowResizeCommand) command).getRowPosition() < getRowCount() - 1) {
            RowResizeCommand rowResizeCommand = (RowResizeCommand) command;
            int newRowHeight = rowResizeCommand.downScaleValue()
                    ? this.rowHeightConfig.downScale(rowResizeCommand.getNewHeight())
                    : rowResizeCommand.getNewHeight();

            setRowHeight(rowResizeCommand.getRowPosition(), newRowHeight);
            fireLayerEvent(new RowResizeEvent(this, rowResizeCommand.getRowPosition()));
            return true;
        } else if (command instanceof MultiRowResizeCommand && command.convertToTargetLayer(this)) {
            MultiRowResizeCommand rowResizeCommand = (MultiRowResizeCommand) command;
            for (int row : rowResizeCommand.getRowPositionsArray()) {
                int newRowHeight = rowResizeCommand.downScaleValue()
                        ? this.rowHeightConfig.downScale(rowResizeCommand.getRowHeight(row))
                        : rowResizeCommand.getRowHeight(row);

                setRowHeight(row, newRowHeight);
                fireLayerEvent(new RowResizeEvent(this, row));
                // do not consume as additional rows might need to get updated
                // too
            }
        }
        return super.doCommand(command);
    }

    // Persistence

    @Override
    public void saveState(String prefix, Properties properties) {
        super.saveState(prefix, properties);
        int level = 0;
        for (GroupModel groupModel : this.model) {
            groupModel.saveState(prefix + PERSISTENCE_KEY_COLUMN_GROUPS + "_" + level, properties); //$NON-NLS-1$
            level++;
        }
    }

    @Override
    public void loadState(String prefix, Properties properties) {
        super.loadState(prefix, properties);
        // expand all currently collapsed groups
        expandAllGroups();
        int level = 0;
        for (GroupModel groupModel : this.model) {
            // load the group model
            groupModel.loadState(prefix + PERSISTENCE_KEY_COLUMN_GROUPS + "_" + level, properties); //$NON-NLS-1$
            // trigger real collapse of collapsed groups in model
            List<Group> collapsedGroups = new ArrayList<>();
            for (Group group : groupModel.getGroups()) {
                if (group.isCollapsed()) {
                    collapsedGroups.add(group);
                }
            }
            if (!collapsedGroups.isEmpty()) {
                doCommand(new ColumnGroupCollapseCommand(groupModel, collapsedGroups));
            }
            level++;
        }

        fireLayerEvent(new ColumnStructuralRefreshEvent(this));
    }

    // Vertical features

    // Rows

    @Override
    public int getRowCount() {
        return this.underlyingLayer.getRowCount() + this.model.size();
    }

    @Override
    public int getPreferredRowCount() {
        return this.underlyingLayer.getPreferredRowCount() + this.model.size();
    }

    @Override
    public int getRowIndexByPosition(int rowPosition) {
        int rowCount = this.model.size();
        if (rowPosition < rowCount) {
            return rowPosition;
        } else {
            return this.underlyingLayer.getRowIndexByPosition(rowPosition - rowCount);
        }
    }

    @Override
    public int localToUnderlyingRowPosition(int localRowPosition) {
        int rowCount = this.model.size();
        if (localRowPosition < rowCount) {
            return localRowPosition;
        } else {
            return localRowPosition - rowCount;
        }
    }

    // Height

    private int getGroupingHeight() {
        if (!this.calculateHeight) {
            return this.rowHeightConfig.getAggregateSize(this.model.size());
        }

        int height = 0;
        for (int i = 0; i < this.model.size(); i++) {
            GroupModel groupModel = this.model.get(i);
            if (!groupModel.isEmpty()) {
                height += this.rowHeightConfig.getSize(getRowPositionForLevel(i));
            }
        }
        return height;
    }

    @Override
    public int getHeight() {
        return getGroupingHeight() + this.underlyingLayer.getHeight();
    }

    @Override
    public int getPreferredHeight() {
        return getGroupingHeight() + this.underlyingLayer.getPreferredHeight();
    }

    @Override
    public int getRowHeightByPosition(int rowPosition) {
        int rowCount = this.model.size();
        if (rowPosition < rowCount) {
            if (!this.calculateHeight) {
                return this.rowHeightConfig.getSize(rowPosition);
            } else {
                int level = getLevelForRowPosition(rowPosition);
                return getGroupModel(level).isEmpty() ? 0 : this.rowHeightConfig.getSize(rowPosition);
            }
        } else {
            return this.underlyingLayer.getRowHeightByPosition(rowPosition - rowCount);
        }
    }

    /**
     * Set the row height for grouping level 0.
     *
     * @param rowHeight
     *            The height to set for grouping level 0.
     */
    public void setRowHeight(int rowHeight) {
        setRowHeight(getRowPositionForLevel(0), rowHeight);
    }

    /**
     * Set the row height for the given row in this layer.
     * <p>
     * <b>Note: </b> Use {@link #getLevelForRowPosition(int)} if the row
     * position for a level needs to be determined.
     * </p>
     *
     * @param row
     *            The row whose height should be set.
     * @param rowHeight
     *            The height to set for the given row position.
     */
    public void setRowHeight(int row, int rowHeight) {
        this.rowHeightConfig.setSize(row, rowHeight);
    }

    /**
     *
     * @param level
     *            The level for which the row position is requested.
     * @return The row positions for the given grouping level.
     */
    public int getRowPositionForLevel(int level) {
        return this.model.size() - level - 1;
    }

    /**
     *
     * @param rowPosition
     *            The row position for which the level is requested.
     * @return The level for the given row position.
     */
    public int getLevelForRowPosition(int rowPosition) {
        return this.model.size() - rowPosition - 1;
    }

    // Row resize

    @Override
    public boolean isRowPositionResizable(int rowPosition) {
        int rowCount = this.model.size();
        if (rowPosition < rowCount) {
            return this.rowHeightConfig.isPositionResizable(rowPosition);
        } else {
            return this.underlyingLayer.isRowPositionResizable(rowPosition - rowCount);
        }
    }

    /**
     * Set the row resizable configuration for the given row position.
     *
     * @param rowPosition
     *            The row for which the resizable flag should be set.
     * @param resizable
     *            <code>true</code> if the row should be resizable,
     *            <code>false</code> if not.
     */
    public void setRowPositionResizable(int rowPosition, boolean resizable) {
        this.rowHeightConfig.setPositionResizable(rowPosition, resizable);
    }

    // Y

    @Override
    public int getRowPositionByY(int y) {
        int groupHeight = getGroupingHeight();
        if (y <= groupHeight) {
            return LayerUtil.getRowPositionByY(this, y);
        } else {
            return this.model.size() + this.underlyingLayer.getRowPositionByY(y - groupHeight);
        }
    }

    @Override
    public int getStartYOfRowPosition(int rowPosition) {
        int rowCount = this.model.size();
        if (rowPosition < rowCount) {
            if (!this.calculateHeight) {
                return this.rowHeightConfig.getAggregateSize(rowPosition);
            } else {
                int startY = 0;
                for (int i = 0; i < rowPosition; i++) {
                    GroupModel groupModel = this.model.get(i);
                    if (!groupModel.isEmpty()) {
                        startY += this.rowHeightConfig.getSize(getRowPositionForLevel(i));
                    }
                }
                return startY;
            }
        } else {
            return getGroupingHeight()
                    + this.underlyingLayer.getStartYOfRowPosition(rowPosition - this.model.size());
        }
    }

    // Cell features

    @Override
    public ILayerCell getCellByPosition(final int columnPosition, final int rowPosition) {
        // Column group header cell
        if (rowPosition < this.model.size()) {
            int level = getLevelForRowPosition(rowPosition);
            Group group = getGroupByPosition(level, columnPosition);
            if (group != null) {
                int start = convertColumnPositionUpwards(getPositionLayer().getColumnPositionByIndex(group.getVisibleStartIndex()));

                // check if there is a level above that does not have a group
                int row = rowPosition;
                int rowSpan = 1;
                while (level < (this.model.size() - 1)) {
                    level++;
                    Group upperGroup = getGroupByPosition(level, columnPosition);
                    if (upperGroup == null) {
                        row--;
                        rowSpan++;
                    } else {
                        break;
                    }
                }

                // if the header should be shown always, e.g. because of
                // huge column groups, the start will not below 0 and the
                // end not below column count
                int columnSpan = getColumnSpan(group);
                if (this.showAlwaysGroupNames) {
                    if (start < 0) {
                        columnSpan += start;
                        start = 0;
                    }

                    if (start + columnSpan > getColumnCount()) {
                        columnSpan = getColumnCount() - start;
                    }
                }

                return new LayerCell(
                        this,
                        start,
                        row,
                        columnPosition,
                        rowPosition,
                        columnSpan,
                        rowSpan);
            } else {
                // for the level there is no group, check if the level below has
                // a group to calculate the row spanning
                int rowSpan = 2;
                Group subGroup = null;
                while (level > 0) {
                    level--;
                    group = getGroupByPosition(level, columnPosition);
                    if (group == null) {
                        rowSpan++;
                    } else {
                        subGroup = group;
                    }
                }

                if (subGroup != null) {
                    int start = convertColumnPositionUpwards(getPositionLayer().getColumnPositionByIndex(subGroup.getVisibleStartIndex()));
                    int columnSpan = getColumnSpan(subGroup);

                    // if the header should be shown always, e.g. because of
                    // huge column groups, the start will not below 0 and the
                    // end not below column count
                    if (this.showAlwaysGroupNames) {
                        if (start < 0) {
                            columnSpan += start;
                            start = 0;
                        }

                        if (start + columnSpan > getColumnCount()) {
                            columnSpan = getColumnCount() - start;
                        }
                    }

                    return new LayerCell(
                            this,
                            start,
                            rowPosition,
                            columnPosition,
                            rowPosition,
                            columnSpan,
                            rowSpan);
                } else {
                    // get the cell from the underlying layer
                    final int span = rowSpan;
                    ILayerCell cell = this.underlyingLayer.getCellByPosition(columnPosition, 0);
                    if (cell != null) {
                        cell = new TransformedLayerCell(cell) {
                            @Override
                            public ILayer getLayer() {
                                return ColumnGroupHeaderLayer.this;
                            }

                            @Override
                            public int getRowSpan() {
                                return span;
                            }

                            @Override
                            public int getRowPosition() {
                                return rowPosition;
                            }

                            @Override
                            public int getOriginRowPosition() {
                                return rowPosition;
                            }
                        };
                    }
                    return cell;
                }
            }
        } else {

            int rowSpan = 1;
            // check for special case if a column header data provider supports
            // multiple rows
            if (rowPosition - 1 < this.model.size()) {
                // check if one row above has a group
                int level = getLevelForRowPosition(rowPosition - 1);

                Group group = null;
                while (level < this.model.size()) {
                    group = getGroupByPosition(level, columnPosition);
                    if (group == null) {
                        rowSpan++;
                    } else {
                        break;
                    }
                    level++;
                }
            }

            final int span = rowSpan;
            ILayerCell cell = this.underlyingLayer.getCellByPosition(columnPosition, 0);
            if (cell != null) {
                cell = new TransformedLayerCell(cell) {
                    @Override
                    public ILayer getLayer() {
                        return ColumnGroupHeaderLayer.this;
                    }

                    @Override
                    public int getRowSpan() {
                        return span;
                    }

                    @Override
                    public int getRowPosition() {
                        return rowPosition;
                    }

                    @Override
                    public int getOriginRowPosition() {
                        return rowPosition - (span - 1);
                    }
                };
            }
            return cell;
        }
    }

    @Override
    public Rectangle getBoundsByPosition(int columnPosition, int rowPosition) {
        Rectangle bounds = super.getBoundsByPosition(columnPosition, rowPosition);
        if (this.compositeFreezeLayer != null && this.compositeFreezeLayer.isFrozen()) {
            // if we are have a composition with freeze and there is a freeze
            // region active, we need to perform some special bound calculation
            // because the origin column positions of a spanned cell could be
            // ambiguous on scrolling
            ILayerCell cell = getCellByPosition(columnPosition, rowPosition);
            int[] columnBounds = this.compositeFreezeLayer.getColumnBounds(
                    columnPosition,
                    cell.getOriginColumnPosition(),
                    cell.getOriginColumnPosition() + cell.getColumnSpan() - 1);
            bounds.x = columnBounds[0];
            bounds.width = columnBounds[1];
        }

        return bounds;
    }

    /**
     * Get the {@link Group} for the column at the given column position for
     * level 0. Will transform the given column position to a position matching
     * the position layer for correct resolution.
     *
     * @param columnPosition
     *            The column position related to this layer.
     * @return The {@link Group} at the given column position or
     *         <code>null</code> if there is no {@link Group} at this position.
     */
    public Group getGroupByPosition(int columnPosition) {
        return getGroupByPosition(0, columnPosition);
    }

    /**
     * Get the {@link Group} for the column at the given column position for the
     * given grouping level. Will transform the given column position to a
     * position matching the position layer for correct resolution.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param columnPosition
     *            The column position related to this layer.
     * @return The {@link Group} at the given column position or
     *         <code>null</code> if there is no {@link Group} at this position.
     */
    public Group getGroupByPosition(int level, int columnPosition) {
        // calculate the position matching the position layer
        int posColumn = LayerUtil.convertColumnPosition(this, columnPosition, getPositionLayer());
        if (posColumn > -1) {
            GroupModel groupModel = getGroupModel(level);
            if (groupModel != null) {
                return groupModel.getGroupByPosition(posColumn);
            }
        }
        return null;
    }

    /**
     * Finds a {@link Group} and its parent {@link GroupModel} based on the
     * coordinates.
     *
     * @param columnPosition
     *            The column position based on the position layer.
     * @param rowPosition
     *            The row position based on this layer.
     * @return Object array where the first item is the {@link GroupModel} and
     *         the second item is the found {@link Group}. Returns
     *         <code>null</code> if either no {@link GroupModel} or no
     *         {@link Group} was found.
     */
    protected Object[] findGroupForCoordinates(int columnPosition, int rowPosition) {
        int level = getLevelForRowPosition(rowPosition);
        GroupModel groupModel = null;
        Group group = null;

        for (; level >= 0; level--) {
            groupModel = getGroupModel(level);
            if (groupModel != null) {
                group = groupModel.getGroupByPosition(columnPosition);
                if (group != null) {
                    return new Object[] { groupModel, group };
                }
            }
        }

        return null;
    }

    /**
     * Checks if there is a {@link Group} configured for the given column
     * position at any level.
     *
     * @param columnPosition
     *            The column position related to this layer.
     * @return <code>true</code> if there is a {@link Group} at the given column
     *         position, <code>false</code> if not.
     */
    public boolean isPartOfAGroup(int columnPosition) {
        for (int level = 0; level < this.model.size(); level++) {
            if (isPartOfAGroup(level, columnPosition)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks if there is a {@link Group} configured for the given column
     * position at the given level.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param columnPosition
     *            The column position related to this layer.
     * @return <code>true</code> if there is a {@link Group} at the given column
     *         position, <code>false</code> if not.
     */
    public boolean isPartOfAGroup(int level, int columnPosition) {
        Group group = getGroupByPosition(level, columnPosition);
        return group != null;
    }

    /**
     * Check if the specified position belongs to a {@link Group} and if this
     * {@link Group} is unbreakable. Convenience method for checks on level 0.
     *
     * @param columnPosition
     *            The position used to retrieve the corresponding group related
     *            to this layer.
     * @return <code>true</code> if the specified position belongs to a
     *         {@link Group} and this {@link Group} is unbreakable,
     *         <code>false</code> if not.
     */
    public boolean isPartOfAnUnbreakableGroup(int columnPosition) {
        return isPartOfAnUnbreakableGroup(0, columnPosition);
    }

    /**
     * Check if the specified position belongs to a {@link Group} at the
     * specified level and if this {@link Group} is unbreakable.
     *
     * @param level
     *            The level for which the check should be performed.
     * @param columnPosition
     *            The position used to retrieve the corresponding group related
     *            to this layer.
     * @return <code>true</code> if the specified position belongs to a
     *         {@link Group} at the specified level and this {@link Group} is
     *         unbreakable, <code>false</code> if not.
     */
    public boolean isPartOfAnUnbreakableGroup(int level, int columnPosition) {
        Group group = getGroupByPosition(level, columnPosition);
        if (group != null) {
            return group.isUnbreakable();
        }
        return false;
    }

    /**
     * Calculates the span of a cell in a group. Takes into account collapsed
     * and hidden columns in the group.
     *
     * @param group
     *            the group for which the span should be calculated.
     */
    public int getColumnSpan(Group group) {
        int sizeOfGroup = group.getVisibleSpan();

        if (group.isCollapsed()) {
            int sizeOfStaticColumns = group.getStaticIndexes().length;
            if (sizeOfStaticColumns == 0) {
                return 1;
            } else {
                int staticSize = 0;
                for (int index : group.getStaticIndexes()) {
                    if (getPositionLayer().getColumnPositionByIndex(index) >= 0) {
                        staticSize++;
                    }
                }
                sizeOfGroup = staticSize;
            }
        }

        return sizeOfGroup;
    }

    @Override
    public DisplayMode getDisplayModeByPosition(int columnPosition, int rowPosition) {
        if (rowPosition < this.model.size() && isPartOfAGroup(getLevelForRowPosition(rowPosition), columnPosition)) {
            return DisplayMode.NORMAL;
        } else {
            return this.underlyingLayer.getDisplayModeByPosition(columnPosition, rowPosition);
        }
    }

    @Override
    public LabelStack getConfigLabelsByPosition(int columnPosition, int rowPosition) {
        int posColumn = LayerUtil.convertColumnPosition(this, columnPosition, getPositionLayer());
        Object[] found = findGroupForCoordinates(posColumn, rowPosition);
        Group group = found != null ? (Group) found[1] : null;

        if (rowPosition < this.model.size() && group != null) {
            LabelStack stack = new LabelStack();
            if (getConfigLabelAccumulator() != null) {
                getConfigLabelAccumulator().accumulateConfigLabels(stack, columnPosition, rowPosition);
            }
            stack.addLabel(GridRegion.COLUMN_GROUP_HEADER);

            if (group != null && group.isCollapseable()) {
                if (group.isCollapsed()) {
                    stack.addLabelOnTop(GroupHeaderConfigLabels.GROUP_COLLAPSED_CONFIG_TYPE);
                } else {
                    stack.addLabelOnTop(GroupHeaderConfigLabels.GROUP_EXPANDED_CONFIG_TYPE);
                }
            }

            return stack;
        } else {
            return this.underlyingLayer.getConfigLabelsByPosition(columnPosition, rowPosition);
        }
    }

    @Override
    public Object getDataValueByPosition(int columnPosition, int rowPosition) {
        if (rowPosition < this.model.size()) {
            int level = getLevelForRowPosition(rowPosition);
            Group group = getGroupByPosition(level, columnPosition);
            while (group == null && level > 0) {
                level--;
                group = getGroupByPosition(level, columnPosition);
            }

            if (group != null) {
                return group.getName();
            }
        }

        return this.underlyingLayer.getDataValueByPosition(columnPosition, 0);
    }

    @Override
    public LabelStack getRegionLabelsByXY(int x, int y) {
        if (y < getGroupingHeight()) {
            for (int i = 0; i < this.model.size(); i++) {
                if (isPartOfAGroup(i, getColumnPositionByX(x))) {
                    return new LabelStack(GridRegion.COLUMN_GROUP_HEADER);
                }
            }
        }

        return this.underlyingLayer.getRegionLabelsByXY(x, y - getGroupingHeight());
    }

    // GroupModel delegates

    /**
     * Returns the {@link Group} for the given name at level 0.
     *
     * @param name
     *            The name of the requested group.
     * @return The group with the given group name or <code>null</code> if there
     *         is no group with such a name.
     */
    public Group getGroupByName(String name) {
        return getGroupByName(0, name);
    }

    /**
     * Returns the {@link Group} for the given name.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param name
     *            The name of the requested group.
     * @return The group with the given group name or <code>null</code> if there
     *         is no group with such a name.
     */
    public Group getGroupByName(int level, String name) {
        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null) {
            return groupModel.getGroupByName(name);
        }
        return null;
    }

    /**
     * Adds the given positions to the group with the given name.
     *
     * @param groupName
     *            The name of the group to which the given positions should be
     *            added.
     * @param positions
     *            The positions to add corresponding to this layer.
     */
    public void addPositionsToGroup(String groupName, int... positions) {
        addPositionsToGroup(0, groupName, positions);
    }

    /**
     * Adds the given positions to the group with the given name.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param groupName
     *            The name of the group to which the given positions should be
     *            added.
     * @param positions
     *            The positions to add corresponding to this layer.
     */
    public void addPositionsToGroup(int level, String groupName, int... positions) {
        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null) {
            Group group = groupModel.getGroupByName(groupName);
            if (group != null) {
                addPositionsToGroup(group, positions);
            }
        }
    }

    /**
     * Adds the given positions to the group to which the given column position
     * belongs to.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param columnPosition
     *            The column position related to this layer to get the
     *            corresponding group to which the given positions should be
     *            added.
     * @param positions
     *            The positions to add corresponding to this layer.
     */
    public void addPositionsToGroup(int level, int columnPosition, int... positions) {
        Group group = getGroupByPosition(level, columnPosition);
        if (group != null) {
            addPositionsToGroup(group, positions);
        }
    }

    /**
     * Adds the given positions to the given {@link Group}.
     *
     * @param group
     *            The {@link Group} to which the positions should be added.
     * @param positions
     *            The positions to add corresponding to this layer.
     */
    protected void addPositionsToGroup(Group group, int... positions) {
        addPositionsToGroup(0, group, positions);
    }

    /**
     * Adds the given positions to the given {@link Group}.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param group
     *            The {@link Group} to which the positions should be added.
     * @param positions
     *            The positions to add corresponding to this layer.
     */
    protected void addPositionsToGroup(int level, Group group, int... positions) {
        int[] converted = new int[positions.length];
        for (int pos = 0; pos < positions.length; pos++) {
            // calculate the position matching the position layer
            converted[pos] = LayerUtil.convertColumnPosition(this, positions[pos], getPositionLayer());
        }

        if (group.isCollapsed()) {
            getPositionLayer().doCommand(new ColumnGroupExpandCommand(getGroupModel(level), group));
        }

        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null) {
            groupModel.addPositionsToGroup(group, converted);
        }

        fireLayerEvent(new RowStructuralRefreshEvent(ColumnGroupHeaderLayer.this.underlyingLayer));
    }

    /**
     * Removes the given positions from corresponding groups. Only performs an
     * action if the position is part of the group.
     * <p>
     * <b>Note:</b><br>
     * A removal will only happen for columns at the beginning or the end of a
     * group. Removing a position in the middle will cause removal of columns at
     * the end of the group to avoid splitting a group.
     * </p>
     * <p>
     * <b>Note:</b><br>
     * A removal does only work for visible positions. That means removing
     * something from a collapsed group does not work.
     * </p>
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param positions
     *            The positions to remove corresponding to this layer.
     */
    public void removePositionsFromGroup(int level, int... positions) {
        int[] converted = new int[positions.length];
        for (int pos = 0; pos < positions.length; pos++) {
            // calculate the position matching the position layer
            converted[pos] = LayerUtil.convertColumnPosition(this, positions[pos], getPositionLayer());
        }

        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null) {
            for (int i = converted.length - 1; i >= 0; i--) {
                int pos = converted[i];
                Group group = groupModel.getGroupByPosition(pos);
                if (group.isCollapsed()) {
                    getPositionLayer().doCommand(new ColumnGroupExpandCommand(groupModel, group));
                }
                groupModel.removePositionsFromGroup(group, pos);
            }
        }

        // fire the event to update column group rendering in case calculate
        // height is enabled
        if (this.calculateHeight) {
            fireLayerEvent(new RowStructuralRefreshEvent(ColumnGroupHeaderLayer.this.underlyingLayer));
        }
    }

    /**
     * Creates and adds a group.
     *
     * @param groupName
     *            The name of the group. Typically used as value in the cell.
     * @param startIndex
     *            The index of the first item in the group.
     * @param span
     *            The configured number of items that belong to this group.
     */
    public void addGroup(String groupName, int startIndex, int span) {
        addGroup(0, groupName, startIndex, span);
    }

    /**
     * Creates and adds a group.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param groupName
     *            The name of the group. Typically used as value in the cell.
     * @param startIndex
     *            The index of the first item in the group.
     * @param span
     *            The configured number of items that belong to this group.
     */
    public void addGroup(int level, String groupName, int startIndex, int span) {
        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null) {
            groupModel.addGroup(groupName, startIndex, span);
        }
    }

    /**
     * Removes the group identified by the given name.
     *
     * @param groupName
     *            The name of the group to remove.
     */
    public void removeGroup(String groupName) {
        removeGroup(0, groupName);
    }

    /**
     * Removes the group identified by the given name.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param groupName
     *            The name of the group to remove.
     */
    public void removeGroup(int level, String groupName) {
        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null) {
            Group group = groupModel.removeGroup(groupName);
            if (group != null && group.isCollapsed()) {
                getPositionLayer().doCommand(new ColumnGroupExpandCommand(getGroupModel(level), group));
            }
        }
    }

    /**
     * Removes the group identified by the given column position.
     *
     * @param columnPosition
     *            The group that contains the given column position.
     */
    public void removeGroup(int columnPosition) {
        removeGroup(0, columnPosition);
    }

    /**
     * Removes the group identified by the given column position.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param columnPosition
     *            The group that contains the given column position.
     */
    public void removeGroup(int level, int columnPosition) {
        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null) {
            Group group = groupModel.removeGroup(columnPosition);
            if (group != null && group.isCollapsed()) {
                getPositionLayer().doCommand(new ColumnGroupExpandCommand(getGroupModel(level), group));
            }
        }
    }

    /**
     * Removes the given group.
     *
     * @param group
     *            The group to remove.
     */
    public void removeGroup(Group group) {
        removeGroup(0, group);
    }

    /**
     * Removes the given group.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param group
     *            The group to remove.
     */
    public void removeGroup(int level, Group group) {
        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null) {
            groupModel.removeGroup(group);
            if (group != null && group.isCollapsed()) {
                getPositionLayer().doCommand(new ColumnGroupExpandCommand(getGroupModel(level), group));
            }
        }
    }

    /**
     * Removes all groups in all levels.
     */
    public void clearAllGroups() {
        for (GroupModel groupModel : this.model) {
            if (!groupModel.isEmpty()) {
                getPositionLayer().doCommand(new ColumnGroupExpandCommand(groupModel, groupModel.getGroups()));
            }
            groupModel.clear();
        }
    }

    /**
     * Removes all groups in the given level.
     *
     * @param level
     *            The grouping level that should be cleared. The level is zero
     *            based bottom-up.
     */
    public void clearAllGroups(int level) {
        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null && !groupModel.isEmpty()) {
            getPositionLayer().doCommand(new ColumnGroupExpandCommand(groupModel, groupModel.getGroups()));
            groupModel.clear();
        }
    }

    /**
     * Adds the given indexes as static indexes to the group that is identified
     * by the given group name. Static indexes are the indexes that stay visible
     * when the group is collapsed.
     *
     * @param groupName
     *            The name of a group to which the the static indexes should be
     *            added to.
     * @param staticIndexes
     *            The static indexes to add.
     */
    public void addStaticColumnIndexesToGroup(String groupName, int... staticIndexes) {
        addStaticColumnIndexesToGroup(0, groupName, staticIndexes);
    }

    /**
     * Adds the given indexes as static indexes to the group that is identified
     * by the given group name. Static indexes are the indexes that stay visible
     * when the group is collapsed.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param groupName
     *            The name of a group to which the the static indexes should be
     *            added to.
     * @param staticIndexes
     *            The static indexes to add.
     */
    public void addStaticColumnIndexesToGroup(int level, String groupName, int... staticIndexes) {
        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null) {
            groupModel.addStaticIndexesToGroup(groupName, staticIndexes);
        }
    }

    /**
     * Adds the given indexes as static indexes to the group that is identified
     * by the given column position. Static indexes are the indexes that stay
     * visible when the group is collapsed.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param columnPosition
     *            The position of a group to which the the static indexes should
     *            be added to.
     * @param staticIndexes
     *            The static indexes to add.
     */
    public void addStaticColumnIndexesToGroup(int level, int columnPosition, int... staticIndexes) {
        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null) {
            groupModel.addStaticIndexesToGroup(columnPosition, staticIndexes);
        }
    }

    /**
     * Collapses the group with the given name.
     *
     * @param groupName
     *            The name of the group that should be collapsed.
     */
    public void collapseGroup(String groupName) {
        collapseGroup(0, groupName);
    }

    /**
     * Collapses the group with the given name.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param groupName
     *            The name of the group that should be collapsed.
     */
    public void collapseGroup(int level, String groupName) {
        collapseGroup(getGroupModel(level), getGroupByName(groupName));
    }

    /**
     * Collapses the group for the given position, if the column at the
     * specified position belongs to a group.
     *
     * @param position
     *            The position corresponding to this layer whose corresponding
     *            group should be collapsed.
     */
    public void collapseGroup(int position) {
        collapseGroup(0, position);
    }

    /**
     * Collapses the group for the given position, if the column at the
     * specified position belongs to a group.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param position
     *            The position corresponding to this layer whose corresponding
     *            group should be collapsed.
     */
    public void collapseGroup(int level, int position) {
        collapseGroup(getGroupModel(level), getGroupByPosition(level, position));
    }

    /**
     * Collapses the given group of the given group model.
     *
     * @param groupModel
     *            The group model to which the given group belongs to.
     * @param group
     *            The group to collapse.
     */
    public void collapseGroup(GroupModel groupModel, Group group) {
        if (groupModel != null && group != null) {
            getPositionLayer().doCommand(new ColumnGroupCollapseCommand(groupModel, group));
        }
    }

    /**
     * Collapses all groups in all levels.
     */
    public void collapseAllGroups() {
        for (GroupModel groupModel : this.model) {
            if (!groupModel.isEmpty()) {
                getPositionLayer().doCommand(new ColumnGroupCollapseCommand(groupModel, groupModel.getGroups()));
            }
        }
    }

    /**
     * Collapses all groups in the given level.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     */
    public void collapseAllGroups(int level) {
        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null && !groupModel.isEmpty()) {
            getPositionLayer().doCommand(new ColumnGroupCollapseCommand(groupModel, groupModel.getGroups()));
        }
    }

    /**
     * Expands the group with the given name.
     *
     * @param groupName
     *            The name of the group that should be expanded.
     */
    public void expandGroup(String groupName) {
        expandGroup(0, groupName);
    }

    /**
     * Expands the group with the given name.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param groupName
     *            The name of the group that should be expanded.
     */
    public void expandGroup(int level, String groupName) {
        expandGroup(getGroupModel(level), getGroupByName(groupName));
    }

    /**
     * Expands the group for the given position, if the column at the specified
     * position belongs to a group.
     *
     * @param position
     *            The position corresponding to this layer whose corresponding
     *            group should be expanded.
     */
    public void expandGroup(int position) {
        expandGroup(0, position);
    }

    /**
     * Expands the group for the given position, if the column at the specified
     * position belongs to a group.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param position
     *            The position corresponding to this layer whose corresponding
     *            group should be expanded.
     */
    public void expandGroup(int level, int position) {
        expandGroup(getGroupModel(level), getGroupByPosition(level, position));
    }

    /**
     * Expands the given group of the given group model.
     *
     * @param groupModel
     *            The group model to which the given group belongs to.
     * @param group
     *            The group to expand.
     */
    public void expandGroup(GroupModel groupModel, Group group) {
        if (groupModel != null && group != null) {
            getPositionLayer().doCommand(new ColumnGroupExpandCommand(groupModel, group));
        }
    }

    /**
     * Expands all groups in all levels.
     */
    public void expandAllGroups() {
        for (GroupModel groupModel : this.model) {
            if (!groupModel.isEmpty()) {
                getPositionLayer().doCommand(new ColumnGroupExpandCommand(groupModel, groupModel.getGroups()));
            }
        }
    }

    /**
     * Expands all groups in the given level.
     *
     * @param level
     *            The grouping level that should be expanded. The level is zero
     *            based bottom-up.
     */
    public void expandAllGroups(int level) {
        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null && !groupModel.isEmpty()) {
            getPositionLayer().doCommand(new ColumnGroupExpandCommand(groupModel, groupModel.getGroups()));
        }
    }

    /**
     * Sets the default value for the collapseable flag when creating group
     * objects for all group levels.
     *
     * @param defaultCollapseable
     *            the default value for the collapseable flag that should be set
     *            on creating group.
     */
    public void setDefaultCollapseable(boolean defaultCollapseable) {
        for (GroupModel groupModel : this.model) {
            groupModel.setDefaultCollapseable(defaultCollapseable);
        }
    }

    /**
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @return The default value for the collapseable flag of newly created
     *         group objects.
     */
    public boolean isDefaultCollapseable(int level) {
        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null) {
            return groupModel.isDefaultCollapseable();
        }
        return false;
    }

    /**
     * Sets the default value for the collapseable flag when creating group
     * objects.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param defaultCollapseable
     *            the default value for the collapseable flag that should be set
     *            on creating group.
     */
    public void setDefaultCollapseable(int level, boolean defaultCollapseable) {
        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null) {
            groupModel.setDefaultCollapseable(defaultCollapseable);
        }
    }

    /**
     * Set the group with the given group name to be collapseable or not.
     *
     * @param groupName
     *            The name of the group that should be modified.
     * @param collabseable
     *            <code>true</code> to set the group collapseable,
     *            <code>false</code> to set it not to be collapseable.
     */
    public void setGroupCollapseable(String groupName, boolean collabseable) {
        setGroupCollapseable(0, groupName, collabseable);
    }

    /**
     * Set the group with the given group name to be collapseable or not.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param groupName
     *            The name of the group that should be modified.
     * @param collabseable
     *            <code>true</code> to set the group collapseable,
     *            <code>false</code> to set it not to be collapseable.
     */
    public void setGroupCollapseable(int level, String groupName, boolean collabseable) {
        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null) {
            groupModel.setGroupCollapseable(groupName, collabseable);
        }
    }

    /**
     * Set the group to which the specified position belongs to, to be
     * collapseable or not.
     *
     * @param position
     *            The position used to retrieve the corresponding group.
     * @param collabseable
     *            <code>true</code> to set the group collapseable,
     *            <code>false</code> to set it not to be collapseable.
     */
    public void setGroupCollapseable(int position, boolean collabseable) {
        setGroupCollapseable(0, position, collabseable);
    }

    /**
     * Set the group to which the specified position belongs to, to be
     * collapseable or not.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param position
     *            The position used to retrieve the corresponding group.
     * @param collabseable
     *            <code>true</code> to set the group collapseable,
     *            <code>false</code> to set it not to be collapseable.
     */
    public void setGroupCollapseable(int level, int position, boolean collabseable) {
        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null) {
            groupModel.setGroupCollapseable(position, collabseable);
        }
    }

    /**
     * Sets the default value for the unbreakable flag when creating group
     * objects for all grouping levels.
     *
     * @param defaultUnbreakable
     *            the default value for the unbreakable flag that should be set
     *            on creating group.
     */
    public void setDefaultUnbreakable(boolean defaultUnbreakable) {
        for (GroupModel groupModel : this.model) {
            groupModel.setDefaultUnbreakable(defaultUnbreakable);
        }
    }

    /**
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @return The default value for the unbreakable flag of newly created group
     *         objects.
     */
    public boolean isDefaultUnbreakable(int level) {
        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null) {
            return groupModel.isDefaultUnbreakable();
        }
        return false;
    }

    /**
     * Sets the default value for the unbreakable flag when creating group
     * objects.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param defaultUnbreakable
     *            the default value for the unbreakable flag that should be set
     *            on creating group.
     */
    public void setDefaultUnbreakable(int level, boolean defaultUnbreakable) {
        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null) {
            groupModel.setDefaultUnbreakable(defaultUnbreakable);
        }
    }

    /**
     * Set the group with the given name to unbreakable/breakable.
     *
     * @param groupName
     *            The name of the group that should be modified.
     * @param unbreakable
     *            <code>true</code> to set the group unbreakable,
     *            <code>false</code> to remove the unbreakable state.
     */
    public void setGroupUnbreakable(String groupName, boolean unbreakable) {
        setGroupUnbreakable(0, groupName, unbreakable);
    }

    /**
     * Set the group with the given name to unbreakable/breakable.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param groupName
     *            The name of the group that should be modified.
     * @param unbreakable
     *            <code>true</code> to set the group unbreakable,
     *            <code>false</code> to remove the unbreakable state.
     */
    public void setGroupUnbreakable(int level, String groupName, boolean unbreakable) {
        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null) {
            groupModel.setGroupUnbreakable(groupName, unbreakable);
        }
    }

    /**
     * Set the group to which the position belongs to unbreakable/breakable.
     *
     * @param position
     *            The position used to retrieve the corresponding group.
     * @param unbreakable
     *            <code>true</code> to set the group unbreakable,
     *            <code>false</code> to remove the unbreakable state.
     */
    public void setGroupUnbreakable(int position, boolean unbreakable) {
        setGroupUnbreakable(0, position, unbreakable);
    }

    /**
     * Set the group to which the position belongs to unbreakable/breakable.
     *
     * @param level
     *            The grouping level for which the group is requested. The level
     *            is zero based bottom-up.
     * @param position
     *            The position used to retrieve the corresponding group.
     * @param unbreakable
     *            <code>true</code> to set the group unbreakable,
     *            <code>false</code> to remove the unbreakable state.
     */
    public void setGroupUnbreakable(int level, int position, boolean unbreakable) {
        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null) {
            groupModel.setGroupUnbreakable(position, unbreakable);
        }
    }

    // height

    /**
     *
     * @return <code>true</code> if the a check is performed whether column
     *         groups are configured or not. If not the height of the layer will
     *         not show additional height for showing column groups.
     *         <code>false</code> if the height should be fixed regardless of
     *         existing column group.
     */
    public boolean isCalculateHeight() {
        return this.calculateHeight;
    }

    /**
     * Configure whether the {@link ColumnGroupHeaderLayer} should calculate the
     * height of the layer dependent on column group configuration or not.
     *
     * @param calculateHeight
     *            <code>true</code> if the layer should check if column groups
     *            are configured and if not, the height of the column header
     *            will not show the double height for showing column groups.
     *            <code>false</code> if the height should be fixed regardless of
     *            existing column group.
     */
    public void setCalculateHeight(boolean calculateHeight) {
        boolean changed = calculateHeight != this.calculateHeight;
        this.calculateHeight = calculateHeight;
        if (changed) {
            this.fireLayerEvent(new RowStructuralRefreshEvent(this));
        }
    }

    // group cell/name rendering

    /**
     *
     * @return <code>true</code> if the group names are always visible on
     *         rendering, e.g. on scrolling, <code>false</code> if the group
     *         names stay at the fixed position in the cell and scroll with the
     *         cell. Default is <code>false</code>.
     */
    public boolean isShowAlwaysGroupNames() {
        return this.showAlwaysGroupNames;
    }

    /**
     * Configure whether group names should be always visible on rendering, e.g.
     * on scrolling, or if the group names should scroll with the cell. Setting
     * this value to <code>true</code> is recommended for huge column groups to
     * ensure that the group name is always visible. This also increases the
     * rendering performance as the spanned grouping cells are limited to the
     * visible area.
     *
     * @param showAlwaysGroupNames
     *            <code>true</code> if the group names should be always visible
     *            on rendering, e.g. on scrolling, <code>false</code> if the
     *            group names should stay at the fixed position in the cell and
     *            scroll with the cell. Default is <code>false</code>.
     */
    public void setShowAlwaysGroupNames(boolean showAlwaysGroupNames) {
        this.showAlwaysGroupNames = showAlwaysGroupNames;
    }

    // reorder

    /**
     * Used to support column reordering via drag and drop. Needed because on
     * drag the viewport could scroll and therefore on drag end the initial
     * position could not be determined anymore.
     *
     * @return The position from which a column reorder operation was started.
     *         Position is based on the configured {@link #positionLayer}.
     */
    public int getReorderFromColumnPosition() {
        return this.reorderFromColumnPosition;
    }

    /**
     * Set the position from which a column group drag operation was started.
     * <p>
     * Used to support column reordering via drag and drop. Needed because on
     * drag the viewport could scroll and therefore on drag end the initial
     * position could not be determined anymore.
     *
     * @param fromColumnPosition
     *            The position from which a column reorder operation was
     *            started. Position needs to be based on the configured
     *            {@link #positionLayer}.
     */
    public void setReorderFromColumnPosition(int fromColumnPosition) {
        this.reorderFromColumnPosition = fromColumnPosition;
    }

    /**
     * Check if reordering for the given grouping level is supported or not. By
     * default reordering is supported on all grouping levels.
     *
     * @param level
     *            The level to check.
     * @return <code>true</code> if the given grouping level does support
     *         reordering, <code>false</code> if group reordering is not
     *         supported for the given level.
     */
    public boolean isReorderSupportedOnLevel(int level) {
        Boolean supported = this.reorderSupportedOnLevel.get(level);
        if (supported != null) {
            return supported;
        }
        return true;
    }

    /**
     * Configure whether reordering for a grouping level should be supported or
     * not. By default reordering is enabled for all grouping levels.
     *
     * @param level
     *            The level for which the reorder support should be configured.
     * @param supported
     *            <code>true</code> if the given grouping level should support
     *            reordering, <code>false</code> if group reordering should not
     *            be supported for the given level.
     */
    public void setReorderSupportedOnLevel(int level, boolean supported) {
        if (level < this.model.size()) {
            this.reorderSupportedOnLevel.put(level, supported);
        }
    }

    /**
     * Reorder a column group for the fromColumnPosition at the given level to
     * the specified toColumnPosition.
     *
     * @param level
     *            The group level on which the group reorder should be
     *            performed.
     * @param fromColumnPosition
     *            The column position of a column in the column group that
     *            should be reordered. Based on the configured
     *            {@link #positionLayer}.
     * @param toColumnPosition
     *            The column position to which a column group should be
     *            reordered to. Based on the configured {@link #positionLayer}.
     * @return <code>true</code> if the reorder command was executed and
     *         consumed successfully
     */
    public boolean reorderColumnGroup(int level, int fromColumnPosition, int toColumnPosition) {
        if (!isReorderSupportedOnLevel(level)
                || !ColumnGroupUtils.isBetweenTwoGroups(
                        this,
                        level,
                        toColumnPosition,
                        toColumnPosition < getPositionLayer().getColumnCount(),
                        PositionUtil.getHorizontalMoveDirection(fromColumnPosition, toColumnPosition))) {

            // consume the command and avoid reordering a group into another
            // group
            return true;
        }

        // additional check if the group itself is part of an unbreakable higher
        // level group
        if (level < getLevelCount() - 1
                && !ColumnGroupUtils.isReorderValid(
                        this,
                        level + 1,
                        fromColumnPosition,
                        toColumnPosition,
                        toColumnPosition < getPositionLayer().getColumnCount())) {
            // consume the command and avoid reordering that breaks an
            // unbreakable group
            return true;
        }

        GroupModel groupModel = getGroupModel(level);
        if (groupModel != null) {
            Group group = groupModel.getGroupByPosition(fromColumnPosition);
            if (group != null) {
                int toPosition = toColumnPosition;

                // we need to convert and fire the command on the underlying
                // layer of the positionLayer as otherwise the special command
                // handler is activated
                int underlyingTo = getPositionLayer().localToUnderlyingColumnPosition(toPosition);

                // we reorder by index so even hidden columns in a group are
                // reordered
                GroupMultiColumnReorderCommand command =
                        new GroupMultiColumnReorderCommand(
                                getPositionLayer(),
                                group.getMembers(),
                                underlyingTo);
                Group toRight = groupModel.getGroupByPosition(toPosition);
                if (toRight != null) {
                    command.setGroupToRight(toRight);
                } else {
                    // check if there is a group to the left
                    Group toLeft = groupModel.getGroupByPosition(toPosition - 1);
                    if (toLeft != null) {
                        command.setGroupToLeft(toLeft);
                    }
                }
                command.setReorderByIndex(true);

                return getPositionLayer().getUnderlyingLayerByPosition(0, 0).doCommand(command);
            }
        }
        return false;
    }

    @Override
    public Collection<String> getProvidedLabels() {
        Collection<String> labels = super.getProvidedLabels();

        labels.add(GridRegion.COLUMN_GROUP_HEADER);
        labels.add(GroupHeaderConfigLabels.GROUP_COLLAPSED_CONFIG_TYPE);
        labels.add(GroupHeaderConfigLabels.GROUP_EXPANDED_CONFIG_TYPE);

        // add the labels configured via IConfigLabelAccumulator
        if (getConfigLabelAccumulator() instanceof IConfigLabelProvider) {
            labels.addAll(((IConfigLabelProvider) getConfigLabelAccumulator()).getProvidedLabels());
        }

        return labels;
    }

    /**
     * {@link ILayerListener} implementation to update {@link Group}s according
     * to structural changes. Is registered on the positionLayer, therefore a
     * position conversion is not necessary.
     */
    private final class StructuralChangeLayerListener implements ILayerListener {

        @Override
        public void handleLayerEvent(ILayerEvent event) {
            // special handling of ColumnReorderEvent because that events
            // removes and adds a column at the same time and we need special
            // handling in terms of left edge to ungroup if possible
            if (event instanceof ColumnReorderEvent) {
                // handle reorder event and update model
                updateColumnGroupModel((ColumnReorderEvent) event);
            } else if (event instanceof IStructuralChangeEvent &&
                    ((IStructuralChangeEvent) event).isHorizontalStructureChanged()) {
                IStructuralChangeEvent changeEvent = (IStructuralChangeEvent) event;
                Collection<StructuralDiff> columnDiffs = changeEvent.getColumnDiffs();
                if (columnDiffs != null && !columnDiffs.isEmpty()) {

                    int[] deletedPositions = getDeletedPositions(columnDiffs);
                    if (deletedPositions != null) {
                        // check if the number of positions are the same as the
                        // number of indexes, otherwise trigger a consistency
                        // check. reason is that the ranges are modified to be
                        // always in a valid range, and that could cause a loss
                        // of hidden positions on conversion
                        if (event instanceof ColumnStructuralChangeEvent
                                && ((ColumnStructuralChangeEvent) event).getColumnIndexes().length > deletedPositions.length) {
                            // this triggers a consistency check
                            handleDeleteDiffs(new int[0]);
                        } else {
                            handleDeleteDiffs(deletedPositions);
                        }
                    }

                    for (StructuralDiff diff : columnDiffs) {
                        if (diff.getDiffType() == DiffTypeEnum.ADD) {
                            // update visible start positions of all groups
                            updateVisibleStartPositions();

                            for (GroupModel groupModel : ColumnGroupHeaderLayer.this.model) {
                                Map<Group, UpdateColumnGroupCollapseCommand> collapseUpdates = new HashMap<>();

                                // find group and update visible span
                                for (int i = diff.getAfterPositionRange().start; i < diff.getAfterPositionRange().end; i++) {
                                    Group group = groupModel.getGroupByPosition(i);
                                    int newStartIndex = getPositionLayer().getColumnIndexByPosition(i);
                                    if (group != null && group.getVisibleStartPosition() <= i) {
                                        if (!group.isCollapsed()) {
                                            group.setVisibleSpan(group.getVisibleSpan() + 1);
                                        } else {
                                            // update collapsed state
                                            UpdateColumnGroupCollapseCommand cmd = collapseUpdates.get(group);
                                            if (cmd == null) {
                                                cmd = new UpdateColumnGroupCollapseCommand(groupModel, group);
                                                collapseUpdates.put(group, cmd);
                                            }
                                            if (!group.containsStaticIndex(newStartIndex)) {
                                                cmd.addIndexesToShow(newStartIndex);
                                                cmd.addIndexesToHide(getPositionLayer().getColumnIndexByPosition(i) + 1);
                                            }
                                            if (group.getVisibleSpan() == 0) {
                                                group.setVisibleSpan(1);
                                            }
                                        }
                                    } else {
                                        // if we have not found a group or the
                                        // found group starts at the new visible
                                        // position, check to the left to ensure
                                        // correct handling at the end of a
                                        // group
                                        Group leftGroup = groupModel.getGroupByPosition(i - 1);
                                        if (leftGroup != null
                                                && leftGroup.getVisibleSpan() < leftGroup.getOriginalSpan()
                                                && leftGroup.hasMember(newStartIndex)) {
                                            if (!leftGroup.isCollapsed()) {
                                                leftGroup.setVisibleSpan(leftGroup.getVisibleSpan() + 1);
                                            } else {
                                                if (!leftGroup.containsStaticIndex(newStartIndex)) {
                                                    // update collapsed state
                                                    UpdateColumnGroupCollapseCommand cmd = collapseUpdates.get(leftGroup);
                                                    if (cmd == null) {
                                                        cmd = new UpdateColumnGroupCollapseCommand(groupModel, leftGroup);
                                                        collapseUpdates.put(leftGroup, cmd);
                                                    }
                                                    cmd.addIndexesToHide(getPositionLayer().getColumnIndexByPosition(i));
                                                } else {
                                                    leftGroup.setVisibleSpan(leftGroup.getVisibleSpan() + 1);
                                                }
                                            }
                                        } else {
                                            // check if there is a group with
                                            // static indexes where the current
                                            // index would belong to
                                            Group g = groupModel.getGroupByStaticIndex(newStartIndex);
                                            if (g != null) {
                                                g.setVisibleStartIndex(newStartIndex);
                                                g.setVisibleSpan(g.getVisibleSpan() + 1);
                                                g.updateVisibleStartPosition();
                                            } else {
                                                // check if there is a group to
                                                // the right and if that group
                                                // has the newStartIndex as
                                                // member
                                                for (int e = diff.getAfterPositionRange().end; e > diff.getAfterPositionRange().start; e--) {
                                                    g = groupModel.getGroupByPosition(e);
                                                    if (g != null && g.hasMember(newStartIndex)) {
                                                        g.setStartIndex(newStartIndex);
                                                        g.setVisibleStartIndex(newStartIndex);
                                                        g.setVisibleSpan(g.getVisibleSpan() + 1);
                                                        g.updateVisibleStartPosition();
                                                        break;
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }

                                for (UpdateColumnGroupCollapseCommand cmd : collapseUpdates.values()) {
                                    doCommand(cmd);
                                }
                            }
                        }
                    }

                    // update visible start positions of all groups
                    updateVisibleStartPositions();
                } else {
                    // trigger a consistency check as the details of the event
                    // probably where removed on converting the layer stack
                    // upwards (e.g. if the hidden position was at the end of
                    // the table) or a complete refresh was triggered which
                    // means there are no diffs
                    performConsistencyCheck(columnDiffs == null);
                }
            }
        }

        private int[] getDeletedPositions(Collection<StructuralDiff> columnDiffs) {
            MutableIntList result = IntLists.mutable.empty();
            boolean deleteDiffFound = false;
            for (StructuralDiff diff : columnDiffs) {
                if (diff.getDiffType() == DiffTypeEnum.DELETE) {
                    deleteDiffFound = true;
                    int[] positions = PositionUtil.getPositions(diff.getBeforePositionRange());
                    result.addAll(positions);
                }
            }
            if (deleteDiffFound) {
                // we need to handle the deleted positions backwards
                return result.sortThis().reverseThis().toArray();
            } else {
                // there was no DELETE diff, so we return null
                return null;
            }
        }

        private void handleDeleteDiffs(int[] positionList) {
            if (positionList.length > 0) {
                for (GroupModel groupModel : ColumnGroupHeaderLayer.this.model) {
                    // we need to create the MutableIntList on a copy of the
                    // positionList array, because modifications on the
                    // MutableIntList will be traversed to the the underlying
                    // array
                    MutableIntList groupPositionList = IntLists.mutable.of(Arrays.copyOf(positionList, positionList.length));
                    while (!groupPositionList.isEmpty()) {
                        // find group and update visible span
                        // we need to iterate because one could hide the
                        // last column in one group and the first of
                        // another group at once
                        Integer pos = groupPositionList.get(0);
                        Group group = groupModel.getGroupByPosition(pos);
                        if (group != null) {
                            // find all positions belonging to
                            // the group
                            MutableIntList groupPositions = IntLists.mutable.of(group.getVisiblePositions());

                            // find the positions in the group
                            // that are hidden
                            MutableIntList hiddenGroupPositions = groupPositions.select(groupPositionList::contains);

                            // update the positionList as we
                            // handled the hidden columns
                            groupPositionList.removeAll(hiddenGroupPositions);

                            groupPositions.removeAll(hiddenGroupPositions);
                            if (groupPositions.size() > 0) {
                                group.setVisibleSpan(groupPositions.size());
                                hiddenGroupPositions.forEach(i -> {
                                    if (group.getVisibleStartPosition() == i) {
                                        group.setVisibleStartIndex(getPositionLayer().getColumnIndexByPosition(i - groupPositionList.size()));
                                    }
                                });
                            } else {
                                group.setVisibleStartIndex(-1);
                                group.setVisibleSpan(0);
                            }
                        } else {
                            groupPositionList.remove(pos);
                        }
                    }
                }
            } else {
                // trigger a consistency check as the details of
                // the event probably where removed on
                // converting the layer stack upwards (e.g. if
                // the hidden position was at the end of the
                // table)
                performConsistencyCheck(false);
            }
        }

        private void updateVisibleStartPositions() {
            for (GroupModel groupModel : ColumnGroupHeaderLayer.this.model) {
                groupModel.updateVisibleStartPositions();
            }
        }

        private void performConsistencyCheck(boolean updateStartIndex) {
            for (GroupModel groupModel : ColumnGroupHeaderLayer.this.model) {
                groupModel.performConsistencyCheck(updateStartIndex);
            }
        }

        private void updateColumnGroupModel(ColumnReorderEvent reorderEvent) {

            int[] fromColumnPositions = PositionUtil.getPositions(reorderEvent.getBeforeFromColumnPositionRanges());
            int toColumnPosition = reorderEvent.getBeforeToColumnPosition();
            boolean reorderToLeftEdge = reorderEvent.isReorderToLeftEdge();

            int fromColumnPosition = toColumnPosition;

            if (toColumnPosition > fromColumnPositions[fromColumnPositions.length - 1]) {
                // Moving from left to right
                fromColumnPosition = fromColumnPositions[fromColumnPositions.length - 1];
            } else if (toColumnPosition < fromColumnPositions[fromColumnPositions.length - 1]) {
                // Moving from right to left
                fromColumnPosition = fromColumnPositions[0];
            }

            MoveDirectionEnum moveDirection = PositionUtil.getHorizontalMoveDirection(fromColumnPosition, toColumnPosition);

            if (reorderToLeftEdge
                    && toColumnPosition > 0
                    && MoveDirectionEnum.RIGHT == moveDirection) {
                toColumnPosition--;
            }

            if (fromColumnPosition == -1 || toColumnPosition == -1) {
                LOG.error("Invalid reorder positions, fromPosition: {}, toPosition: {}", fromColumnPosition, toColumnPosition); //$NON-NLS-1$
                return;
            }

            for (GroupModel groupModel : ColumnGroupHeaderLayer.this.model) {
                Group toColumnGroup = groupModel.getGroupByPosition(toColumnPosition);

                Group fromColumnGroup = groupModel.getGroupByPosition(fromColumnPosition);

                if (fromColumnGroup != null
                        && toColumnGroup != null
                        && fromColumnGroup.equals(toColumnGroup)) {
                    // movement inside a column group

                    // if from and to position are the same and it is a
                    // column at the edge of a group, remove the
                    // position from the group
                    if (fromColumnPosition == toColumnPosition
                            && (!ColumnGroupUtils.isGroupReordered(fromColumnGroup, fromColumnPositions) || (fromColumnGroup.isCollapsed() && fromColumnGroup.getMembers().length > 1))
                            && (fromColumnGroup.isGroupStart(fromColumnPosition)
                                    || fromColumnGroup.isGroupEnd(fromColumnPosition))) {
                        if (MoveDirectionEnum.RIGHT == moveDirection) {
                            int pos[] = new int[fromColumnPositions.length];
                            int index = 0;
                            for (int from = fromColumnPositions.length - 1; from >= 0; from--) {
                                pos[index] = fromColumnPositions[from];
                                index++;
                            }
                            removePositionsFromGroup(
                                    groupModel,
                                    fromColumnGroup,
                                    pos,
                                    reorderEvent.getBeforeFromColumnIndexesArray(),
                                    fromColumnPosition,
                                    moveDirection);
                        } else {
                            removePositionsFromGroup(
                                    groupModel,
                                    fromColumnGroup,
                                    fromColumnPositions,
                                    reorderEvent.getBeforeFromColumnIndexesArray(),
                                    fromColumnPosition,
                                    moveDirection);
                        }
                        break;
                    }

                    // if we moved the first visible start position or
                    // moved another column to the visible start
                    // position, we need to update the start index
                    if (fromColumnPosition != toColumnPosition
                            && (fromColumnGroup.getVisibleStartPosition() == fromColumnPositions[0]
                                    || toColumnGroup.getVisibleStartPosition() == toColumnPosition)) {
                        int newStartIndex = getPositionLayer().getColumnIndexByPosition(fromColumnGroup.getVisibleStartPosition());
                        fromColumnGroup.setVisibleStartIndex(newStartIndex);
                        if (!fromColumnGroup.isCollapsed()
                                || fromColumnGroup.containsStaticIndex(fromColumnGroup.getStartIndex())) {
                            // if we move inside a collapsed group, there are
                            // static indexes, and therefore we do not update
                            // the start index, as the start index should be the
                            // same on expand
                            fromColumnGroup.setStartIndex(newStartIndex);
                        }
                    }
                } else if (fromColumnGroup == null
                        && toColumnGroup != null) {
                    addPositionsToGroup(
                            groupModel,
                            toColumnGroup,
                            fromColumnPositions,
                            reorderEvent.getBeforeFromColumnIndexesArray(),
                            toColumnPosition,
                            moveDirection);
                } else if (fromColumnGroup != null
                        && toColumnGroup == null
                        && !ColumnGroupUtils.isGroupReordered(fromColumnGroup, fromColumnPositions)) {
                    removePositionsFromGroup(
                            groupModel,
                            fromColumnGroup,
                            fromColumnPositions,
                            reorderEvent.getBeforeFromColumnIndexesArray(),
                            fromColumnPosition,
                            moveDirection);
                } else if (fromColumnGroup == null
                        && toColumnGroup == null
                        && fromColumnPosition == toColumnPosition
                        && fromColumnPositions.length == 1) {
                    // this might happen on drag and drop operations when
                    // trying to add a column back into an adjacent column
                    // group
                    int adjacentPos = (moveDirection == MoveDirectionEnum.RIGHT) ? fromColumnPosition + 1 : fromColumnPosition - 1;
                    // check if there is an adjacent column group
                    Group adjacentColumnGroup = groupModel.getGroupByPosition(adjacentPos);
                    if (adjacentColumnGroup != null && !adjacentColumnGroup.isUnbreakable()) {
                        addPositionsToGroup(
                                groupModel,
                                adjacentColumnGroup,
                                fromColumnPositions,
                                reorderEvent.getBeforeFromColumnIndexesArray(),
                                (adjacentColumnGroup.isCollapsed() && moveDirection != MoveDirectionEnum.RIGHT) ? toColumnPosition : adjacentPos,
                                moveDirection);
                    }
                } else if (fromColumnGroup != null
                        && toColumnGroup != null
                        && !fromColumnGroup.equals(toColumnGroup)
                        && (!ColumnGroupUtils.isGroupReordered(fromColumnGroup, fromColumnPositions) || (!fromColumnGroup.isCollapsed() && fromColumnGroup.getVisiblePositions().length == 1))) {

                    removePositionsFromGroup(
                            groupModel,
                            fromColumnGroup,
                            fromColumnPositions,
                            reorderEvent.getBeforeFromColumnIndexesArray(),
                            fromColumnPositions[0],
                            moveDirection);
                    addPositionsToGroup(
                            groupModel,
                            toColumnGroup,
                            fromColumnPositions,
                            reorderEvent.getBeforeFromColumnIndexesArray(),
                            toColumnPosition,
                            moveDirection);
                }

                groupModel.updateVisibleStartPositions();
            }
        }

        private void addPositionsToGroup(
                GroupModel groupModel,
                Group group,
                int[] fromColumnPositions,
                int[] fromColumnIndexes,
                int toPosition,
                MoveDirectionEnum moveDirection) {

            if (!group.isUnbreakable()) {
                // increase the span as column is moved inside group
                group.setOriginalSpan(group.getOriginalSpan() + fromColumnPositions.length);

                // update the start index
                if (group.isGroupStart(toPosition)) {
                    int newStartIndex = (moveDirection == MoveDirectionEnum.RIGHT)
                            ? getPositionLayer().getColumnIndexByPosition(group.getVisibleStartPosition() - fromColumnPositions.length)
                            : getPositionLayer().getColumnIndexByPosition(group.getVisibleStartPosition());

                    if (group.getVisibleStartIndex() == group.getStartIndex()) {
                        group.setStartIndex(newStartIndex);
                    }

                    group.setVisibleStartIndex(newStartIndex);
                }

                // add the member indexes
                group.addMembers(fromColumnIndexes);

                if (group.isCollapsed()) {
                    // update collapsed state
                    UpdateColumnGroupCollapseCommand cmd = new UpdateColumnGroupCollapseCommand(groupModel, group);
                    if (group.isGroupStart(toPosition)) {
                        cmd.addIndexesToHide(group.getMembers());

                        if (group.getStaticIndexes().length > 0) {
                            group.setVisibleSpan(group.getVisibleSpan() + fromColumnPositions.length);
                        }

                        // the update command will trigger a hide event to hide
                        // the previous visible first column, therefore we need
                        // to update the visible start position before firing
                        // the command to ensure the hide handling is operating
                        // on the correct positions
                        group.updateVisibleStartPosition();
                    } else {
                        cmd.addIndexesToHide(fromColumnIndexes);
                        group.setVisibleSpan(group.getVisibleSpan() + fromColumnPositions.length);
                    }
                    getPositionLayer().doCommand(cmd);
                } else {
                    // we update the visible span only in case the group is not
                    // collapsed
                    group.setVisibleSpan(group.getVisibleSpan() + fromColumnPositions.length);
                }
            }
        }

        private void removePositionsFromGroup(
                GroupModel groupModel,
                Group group,
                int[] fromColumnPositions,
                int[] fromColumnIndexes,
                int fromColumnPosition,
                MoveDirectionEnum moveDirection) {

            if (!group.isUnbreakable()) {
                boolean collapsed = group.isCollapsed();
                if (collapsed) {
                    int fromColumnIndex = getPositionLayer().getColumnIndexByPosition(fromColumnPosition);
                    // we need to expand to make the next column visible again
                    expandGroup(groupModel, group);
                    fromColumnPosition = getPositionLayer().getColumnPositionByIndex(fromColumnIndex);
                }

                // decrease the span as column is moved out of group
                group.setOriginalSpan(group.getOriginalSpan() - fromColumnPositions.length);
                group.setVisibleSpan(group.getVisibleSpan() - fromColumnPositions.length);

                // update the start index
                if (group.isGroupStart(fromColumnPosition)) {
                    int newStartIndex = (moveDirection == MoveDirectionEnum.RIGHT)
                            ? getPositionLayer().getColumnIndexByPosition(group.getVisibleStartPosition())
                            : getPositionLayer().getColumnIndexByPosition(group.getVisibleStartPosition() + fromColumnPositions.length);
                    group.setStartIndex(newStartIndex);
                    group.setVisibleStartIndex(newStartIndex);
                }

                // remove the member indexes
                group.removeMembers(fromColumnIndexes);

                // remove static index if removed position was a static index
                group.removeStaticIndexes(ArrayUtil.asIntArray(fromColumnIndexes));

                if (group.getOriginalSpan() > 0) {
                    group.updateVisibleStartPosition();

                    if (collapsed) {
                        // collapse again
                        collapseGroup(groupModel, group);
                    }
                } else {
                    // all members where removed from the group, so we remove
                    // the group itself
                    groupModel.removeGroup(group);
                }
            }
        }
    }
}
