/*******************************************************************************
 * Copyright (c) 2012, 2020 Original authors and others.
 *
 * 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:
 *     Original authors and others - initial API and implementation
 *     Dirk Fauth <dirk.fauth@googlemail.com> - Bug 451152, 453055, 455949
 *     drgler <dsp@bdal.de> - Bug 473532
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.layer;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Properties;

import org.eclipse.nebula.widgets.nattable.command.AbstractRegionCommand;
import org.eclipse.nebula.widgets.nattable.command.ILayerCommand;
import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.coordinate.Range;
import org.eclipse.nebula.widgets.nattable.layer.cell.AggregateConfigLabelAccumulator;
import org.eclipse.nebula.widgets.nattable.layer.cell.IConfigLabelAccumulator;
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.TranslatedLayerCell;
import org.eclipse.nebula.widgets.nattable.painter.cell.ICellPainter;
import org.eclipse.nebula.widgets.nattable.painter.layer.ILayerPainter;
import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
import org.eclipse.nebula.widgets.nattable.ui.binding.UiBindingRegistry;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;

/**
 * A composite layer is a layer that is made up of a number of underlying child
 * layers. This class assumes that the child layers are laid out in a regular
 * grid pattern where the child layers in each composite row all have the same
 * number of rows and the same height, and the child layers in each composite
 * column each have the same number of columns and the same width.
 */
public class CompositeLayer extends AbstractLayer {

    private final int layoutXCount;

    private final int layoutYCount;

    private final HashMap<ILayer, String> childLayerToRegionNameMap = new HashMap<>();
    private final HashMap<String, ILayer> regionNameToChildLayerMap = new HashMap<>();

    private final HashMap<String, IConfigLabelAccumulator> regionNameToConfigLabelAccumulatorMap = new HashMap<>();

    /** Data struct. for child Layers */
    private final ILayer[][] childLayerLayout;

    private final CompositeLayerPainter compositeLayerPainter = new CompositeLayerPainter();

    public CompositeLayer(int layoutXCount, int layoutYCount) {
        this.layoutXCount = layoutXCount;
        this.layoutYCount = layoutYCount;
        this.childLayerLayout = new ILayer[layoutXCount][layoutYCount];

        setLayerPainter(this.compositeLayerPainter);
    }

    // Dispose

    @Override
    public void dispose() {
        for (int layoutX = 0; layoutX < this.layoutXCount; layoutX++) {
            for (int layoutY = 0; layoutY < this.layoutYCount; layoutY++) {
                ILayer childLayer = this.childLayerLayout[layoutX][layoutY];
                if (childLayer != null) {
                    childLayer.dispose();
                }
            }
        }
    }

    // Persistence

    @Override
    public void saveState(String prefix, Properties properties) {
        for (int layoutX = 0; layoutX < this.layoutXCount; layoutX++) {
            for (int layoutY = 0; layoutY < this.layoutYCount; layoutY++) {
                ILayer childLayer = this.childLayerLayout[layoutX][layoutY];
                if (childLayer != null) {
                    String regionName = this.childLayerToRegionNameMap.get(childLayer);
                    childLayer.saveState(prefix + "." + regionName, properties); //$NON-NLS-1$
                }
            }
        }
        super.saveState(prefix, properties);
    }

    @Override
    public void loadState(String prefix, Properties properties) {
        for (int layoutX = 0; layoutX < this.layoutXCount; layoutX++) {
            for (int layoutY = 0; layoutY < this.layoutYCount; layoutY++) {
                ILayer childLayer = this.childLayerLayout[layoutX][layoutY];
                if (childLayer != null) {
                    String regionName = this.childLayerToRegionNameMap.get(childLayer);
                    childLayer.loadState(prefix + "." + regionName, properties); //$NON-NLS-1$
                }
            }
        }
        super.loadState(prefix, properties);
    }

    // Configuration

    @Override
    public void configure(IConfigRegistry configRegistry, UiBindingRegistry uiBindingRegistry) {
        for (int layoutX = 0; layoutX < this.layoutXCount; layoutX++) {
            for (int layoutY = 0; layoutY < this.layoutYCount; layoutY++) {
                this.childLayerLayout[layoutX][layoutY].configure(
                        configRegistry, uiBindingRegistry);
            }
        }

        super.configure(configRegistry, uiBindingRegistry);
    }

    /**
     * {@inheritDoc} Handle commands
     */
    @Override
    public boolean doCommand(ILayerCommand command) {
        if (super.doCommand(command)) {
            return true;
        }

        if (command instanceof AbstractRegionCommand
                && ((AbstractRegionCommand) command).label != null) {
            AbstractRegionCommand arc = ((AbstractRegionCommand) command);
            ILayer child = getChildLayerByRegionName(arc.label);
            if (child != null) {
                // execute and return true because the command was executed for
                // a specific region that has been found, so nobody else should
                // handle it further
                child.doCommand(arc.cloneForRegion());
                return true;
            }
        }

        return doCommandOnChildLayers(command);
    }

    protected boolean doCommandOnChildLayers(ILayerCommand command) {
        for (int layoutX = 0; layoutX < this.layoutXCount; layoutX++) {
            for (int layoutY = 0; layoutY < this.layoutYCount; layoutY++) {
                ILayer childLayer = this.childLayerLayout[layoutX][layoutY];
                ILayerCommand childCommand = command.cloneCommand();
                if (childLayer.doCommand(childCommand)) {
                    return true;
                }
            }
        }
        return false;
    }

    // Horizontal features

    // Columns

    /**
     * @return total number of columns being displayed Note: Works off the
     *         header layers.
     */
    @Override
    public int getColumnCount() {
        int columnCount = 0;
        for (int layoutX = 0; layoutX < this.layoutXCount; layoutX++) {
            columnCount += this.childLayerLayout[layoutX][0].getColumnCount();
        }
        return columnCount;
    }

    @Override
    public int getPreferredColumnCount() {
        int preferredColumnCount = 0;
        for (int layoutX = 0; layoutX < this.layoutXCount; layoutX++) {
            preferredColumnCount += this.childLayerLayout[layoutX][0].getPreferredColumnCount();
        }
        return preferredColumnCount;
    }

    /**
     * @param compositeColumnPosition
     *            Column position in the {@link CompositeLayer}
     * @return column index in the underlying layer.
     */
    @Override
    public int getColumnIndexByPosition(int compositeColumnPosition) {
        int layoutX = getLayoutXByColumnPosition(compositeColumnPosition);
        if (layoutX < 0) {
            return -1;
        }
        ILayer childLayer = this.childLayerLayout[layoutX][0];
        int childColumnPosition = compositeColumnPosition - getColumnPositionOffset(layoutX);
        return childLayer.getColumnIndexByPosition(childColumnPosition);
    }

    @Override
    public int localToUnderlyingColumnPosition(int localColumnPosition) {
        int layoutX = getLayoutXByColumnPosition(localColumnPosition);
        if (layoutX < 0) {
            return -1;
        }
        return localColumnPosition - getColumnPositionOffset(layoutX);
    }

    @Override
    public int underlyingToLocalColumnPosition(
            ILayer sourceUnderlyingLayer, int underlyingColumnPosition) {
        Point layoutCoordinate = getLayoutXYByChildLayer(sourceUnderlyingLayer);
        if (layoutCoordinate == null) {
            return -1;
        }
        return getColumnPositionOffset(layoutCoordinate.x) + underlyingColumnPosition;
    }

    @Override
    public Collection<Range> underlyingToLocalColumnPositions(
            ILayer sourceUnderlyingLayer, Collection<Range> underlyingColumnPositionRanges) {
        Point layoutCoordinate = getLayoutXYByChildLayer(sourceUnderlyingLayer);
        if (layoutCoordinate == null) {
            return null;
        }

        Collection<Range> localColumnPositionRanges = new ArrayList<>(underlyingColumnPositionRanges.size());

        int offset = getColumnPositionOffset(layoutCoordinate.x);
        for (Range underlyingColumnPositionRange : underlyingColumnPositionRanges) {
            localColumnPositionRanges.add(new Range(
                    offset + underlyingColumnPositionRange.start,
                    offset + underlyingColumnPositionRange.end));
        }

        return localColumnPositionRanges;
    }

    // Width

    @Override
    public int getWidth() {
        int width = 0;
        for (int layoutX = 0; layoutX < this.layoutXCount; layoutX++) {
            width += this.childLayerLayout[layoutX][0].getWidth();
        }
        return width;
    }

    @Override
    public int getPreferredWidth() {
        int preferredWidth = 0;
        for (int layoutX = 0; layoutX < this.layoutXCount; layoutX++) {
            preferredWidth += this.childLayerLayout[layoutX][0].getPreferredWidth();
        }
        return preferredWidth;
    }

    @Override
    public int getColumnWidthByPosition(int column) {
        int layoutX = getLayoutXByColumnPosition(column);
        if (layoutX < 0) {
            return 0;
        }
        ILayer childLayer = this.childLayerLayout[layoutX][0];
        return childLayer.getColumnWidthByPosition(column - getColumnPositionOffset(layoutX));
    }

    // Column resize

    @Override
    public boolean isColumnPositionResizable(int compositeColumnPosition) {
        // Only looks at the header
        int layoutX = getLayoutXByColumnPosition(compositeColumnPosition);
        if (layoutX < 0) {
            return false;
        }
        ILayer childLayer = this.childLayerLayout[layoutX][0];
        int childColumnPosition = compositeColumnPosition - getColumnPositionOffset(layoutX);
        return childLayer.isColumnPositionResizable(childColumnPosition);
    }

    // X

    /**
     * @param x
     *            pixel position - starts from 0
     */
    @Override
    public int getColumnPositionByX(int x) {
        int layoutX = getLayoutXByPixelX(x);
        if (layoutX < 0) {
            return -1;
        }
        ILayer childLayer = this.childLayerLayout[layoutX][0];
        int childX = x - getWidthOffset(layoutX);
        int childColumnPosition = childLayer.getColumnPositionByX(childX);
        return getColumnPositionOffset(layoutX) + childColumnPosition;
    }

    @Override
    public int getStartXOfColumnPosition(int columnPosition) {
        int layoutX = getLayoutXByColumnPosition(columnPosition);
        if (layoutX < 0) {
            return -1;
        }
        ILayer childLayer = this.childLayerLayout[layoutX][0];
        int childColumnPosition = columnPosition - getColumnPositionOffset(layoutX);
        return getWidthOffset(layoutX) + childLayer.getStartXOfColumnPosition(childColumnPosition);
    }

    // Underlying

    @Override
    public Collection<ILayer> getUnderlyingLayersByColumnPosition(int columnPosition) {
        Collection<ILayer> underlyingLayers = new HashSet<>();

        for (int layoutX = 0; layoutX < this.childLayerLayout.length; layoutX++) {
            int columnPositionOffset = getColumnPositionOffset(layoutX);
            if (columnPosition >= columnPositionOffset
                    && columnPosition < columnPositionOffset + this.childLayerLayout[layoutX][0].getColumnCount()) {
                for (int layoutY = 0; layoutY < this.childLayerLayout[layoutX].length; layoutY++) {
                    underlyingLayers.add(this.childLayerLayout[layoutX][layoutY]);
                }
                break;
            }
        }

        return underlyingLayers;
    }

    // Vertical features

    // Rows

    @Override
    public int getRowCount() {
        int rowCount = 0;
        for (int layoutY = 0; layoutY < this.layoutYCount; layoutY++) {
            rowCount += this.childLayerLayout[0][layoutY].getRowCount();
        }
        return rowCount;
    }

    @Override
    public int getPreferredRowCount() {
        int preferredRowCount = 0;
        for (int layoutY = 0; layoutY < this.layoutYCount; layoutY++) {
            preferredRowCount += this.childLayerLayout[0][layoutY].getPreferredRowCount();
        }
        return preferredRowCount;
    }

    @Override
    public int getRowIndexByPosition(int compositeRowPosition) {
        int layoutY = getLayoutYByRowPosition(compositeRowPosition);
        if (layoutY < 0) {
            return -1;
        }
        ILayer childLayer = this.childLayerLayout[0][layoutY];
        int childRowPosition = compositeRowPosition - getRowPositionOffset(layoutY);
        return childLayer.getRowIndexByPosition(childRowPosition);

    }

    @Override
    public int localToUnderlyingRowPosition(int localRowPosition) {
        int layoutY = getLayoutYByRowPosition(localRowPosition);
        if (layoutY < 0) {
            return -1;
        }
        return localRowPosition - getRowPositionOffset(layoutY);
    }

    @Override
    public int underlyingToLocalRowPosition(
            ILayer sourceUnderlyingLayer, int underlyingRowPosition) {
        Point layoutCoordinate = getLayoutXYByChildLayer(sourceUnderlyingLayer);
        if (layoutCoordinate == null) {
            return -1;
        }
        return getRowPositionOffset(layoutCoordinate.y) + underlyingRowPosition;
    }

    @Override
    public Collection<Range> underlyingToLocalRowPositions(
            ILayer sourceUnderlyingLayer, Collection<Range> underlyingRowPositionRanges) {
        Point layoutCoordinate = getLayoutXYByChildLayer(sourceUnderlyingLayer);
        if (layoutCoordinate == null) {
            return null;
        }

        Collection<Range> localRowPositionRanges = new ArrayList<>(underlyingRowPositionRanges.size());

        int offset = getRowPositionOffset(layoutCoordinate.y);
        for (Range underlyingRowPositionRange : underlyingRowPositionRanges) {
            localRowPositionRanges.add(new Range(
                    offset + underlyingRowPositionRange.start,
                    offset + underlyingRowPositionRange.end));
        }

        return localRowPositionRanges;
    }

    // Height

    @Override
    public int getHeight() {
        int height = 0;
        for (int layoutY = 0; layoutY < this.layoutYCount; layoutY++) {
            height += this.childLayerLayout[0][layoutY].getHeight();
        }
        return height;
    }

    @Override
    public int getPreferredHeight() {
        int preferredHeight = 0;
        for (int layoutY = 0; layoutY < this.layoutYCount; layoutY++) {
            preferredHeight += this.childLayerLayout[0][layoutY].getPreferredHeight();
        }
        return preferredHeight;
    }

    @Override
    public int getRowHeightByPosition(int row) {
        int layoutY = getLayoutYByRowPosition(row);
        if (layoutY < 0) {
            return 0;
        }
        ILayer childLayer = this.childLayerLayout[0][layoutY];
        return childLayer.getRowHeightByPosition(row - getRowPositionOffset(layoutY));
    }

    // Row resize

    /**
     * @return false if the row position is out of bounds
     */
    @Override
    public boolean isRowPositionResizable(int compositeRowPosition) {
        int layoutY = getLayoutYByRowPosition(compositeRowPosition);
        if (layoutY < 0) {
            return false;
        }
        ILayer childLayer = this.childLayerLayout[0][layoutY];
        int childRowPosition = compositeRowPosition - getRowPositionOffset(layoutY);
        return childLayer.isRowPositionResizable(childRowPosition);
    }

    // Y

    /**
     * Get the <i>row</i> position relative to the layer the containing
     * coordinate y.
     *
     * @param y
     *            Mouse event Y position.
     */
    @Override
    public int getRowPositionByY(int y) {
        int layoutY = getLayoutYByPixelY(y);
        if (layoutY < 0) {
            return -1;
        }
        ILayer childLayer = this.childLayerLayout[0][layoutY];
        int childY = y - getHeightOffset(layoutY);
        int childRowPosition = childLayer.getRowPositionByY(childY);
        return getRowPositionOffset(layoutY) + childRowPosition;
    }

    @Override
    public int getStartYOfRowPosition(int rowPosition) {
        int layoutY = getLayoutYByRowPosition(rowPosition);
        if (layoutY < 0) {
            return -1;
        }
        ILayer childLayer = this.childLayerLayout[0][layoutY];
        int childRowPosition = rowPosition - getRowPositionOffset(layoutY);
        return getHeightOffset(layoutY) + childLayer.getStartYOfRowPosition(childRowPosition);
    }

    @Override
    public Collection<ILayer> getUnderlyingLayersByRowPosition(int rowPosition) {
        Collection<ILayer> underlyingLayers = new HashSet<>();

        for (int layoutY = 0; layoutY < this.childLayerLayout[0].length; layoutY++) {
            int rowPositionOffset = getRowPositionOffset(layoutY);
            if (rowPosition >= rowPositionOffset
                    && rowPosition < rowPositionOffset + this.childLayerLayout[0][layoutY].getRowCount()) {
                for (int layoutX = 0; layoutX < this.childLayerLayout.length; layoutX++) {
                    underlyingLayers.add(this.childLayerLayout[layoutX][layoutY]);
                }
                break;
            }
        }

        return underlyingLayers;
    }

    // Cell features

    @Override
    public ILayerCell getCellByPosition(
            int compositeColumnPosition, int compositeRowPosition) {
        Point layoutCoordinate = getLayoutXYByPosition(
                compositeColumnPosition, compositeRowPosition);

        if (layoutCoordinate == null) {
            return null;
        }

        ILayer childLayer = this.childLayerLayout[layoutCoordinate.x][layoutCoordinate.y];
        int childColumnPosition = compositeColumnPosition - getColumnPositionOffset(layoutCoordinate.x);
        int childRowPosition = compositeRowPosition - getRowPositionOffset(layoutCoordinate.y);

        ILayerCell cell = childLayer.getCellByPosition(childColumnPosition, childRowPosition);

        if (cell != null) {
            cell = new TranslatedLayerCell(
                    cell,
                    this,
                    underlyingToLocalColumnPosition(childLayer, cell.getOriginColumnPosition()),
                    underlyingToLocalRowPosition(childLayer, cell.getOriginRowPosition()),
                    underlyingToLocalColumnPosition(childLayer, cell.getColumnPosition()),
                    underlyingToLocalRowPosition(childLayer, cell.getRowPosition()));
        }

        return cell;
    }

    @Override
    public Rectangle getBoundsByPosition(
            int compositeColumnPosition, int compositeRowPosition) {
        Point layoutCoordinate = getLayoutXYByPosition(
                compositeColumnPosition, compositeRowPosition);

        if (layoutCoordinate == null) {
            return null;
        }

        ILayer childLayer = this.childLayerLayout[layoutCoordinate.x][layoutCoordinate.y];
        int childColumnPosition = compositeColumnPosition - getColumnPositionOffset(layoutCoordinate.x);
        int childRowPosition = compositeRowPosition - getRowPositionOffset(layoutCoordinate.y);

        final Rectangle bounds = childLayer.getBoundsByPosition(
                childColumnPosition, childRowPosition);

        if (bounds != null) {
            bounds.x += getWidthOffset(layoutCoordinate.x);
            bounds.y += getHeightOffset(layoutCoordinate.y);
        }

        return bounds;
    }

    @Override
    public DisplayMode getDisplayModeByPosition(
            int compositeColumnPosition, int compositeRowPosition) {
        Point layoutCoordinate = getLayoutXYByPosition(
                compositeColumnPosition, compositeRowPosition);
        if (layoutCoordinate == null) {
            return super.getDisplayModeByPosition(
                    compositeColumnPosition, compositeRowPosition);
        }
        ILayer childLayer = this.childLayerLayout[layoutCoordinate.x][layoutCoordinate.y];

        return childLayer.getDisplayModeByPosition(
                compositeColumnPosition - getColumnPositionOffset(layoutCoordinate.x),
                compositeRowPosition - getRowPositionOffset(layoutCoordinate.y));
    }

    @Override
    public LabelStack getConfigLabelsByPosition(
            int compositeColumnPosition, int compositeRowPosition) {
        Point layoutCoordinate = getLayoutXYByPosition(compositeColumnPosition, compositeRowPosition);
        if (layoutCoordinate == null) {
            return new LabelStack();
        }
        ILayer childLayer = this.childLayerLayout[layoutCoordinate.x][layoutCoordinate.y];

        int childColumnPosition = compositeColumnPosition - getColumnPositionOffset(layoutCoordinate.x);
        int childRowPosition = compositeRowPosition - getRowPositionOffset(layoutCoordinate.y);
        LabelStack configLabels = childLayer.getConfigLabelsByPosition(
                childColumnPosition, childRowPosition);

        String regionName = this.childLayerToRegionNameMap.get(childLayer);
        IConfigLabelAccumulator configLabelAccumulator = this.regionNameToConfigLabelAccumulatorMap.get(regionName);
        if (configLabelAccumulator != null) {
            configLabelAccumulator.accumulateConfigLabels(
                    configLabels, childColumnPosition, childRowPosition);
        }
        configLabels.addLabel(regionName);

        return configLabels;
    }

    @Override
    public Object getDataValueByPosition(
            int compositeColumnPosition, int compositeRowPosition) {
        Point layoutCoordinate = getLayoutXYByPosition(
                compositeColumnPosition, compositeRowPosition);
        if (layoutCoordinate == null) {
            return null;
        }

        ILayer childLayer = this.childLayerLayout[layoutCoordinate.x][layoutCoordinate.y];
        return childLayer.getDataValueByPosition(
                compositeColumnPosition - getColumnPositionOffset(layoutCoordinate.x),
                compositeRowPosition - getRowPositionOffset(layoutCoordinate.y));
    }

    @Override
    public ICellPainter getCellPainter(
            int compositeColumnPosition, int compositeRowPosition,
            ILayerCell cell, IConfigRegistry configRegistry) {
        Point layoutCoordinate = getLayoutXYByPosition(
                compositeColumnPosition, compositeRowPosition);
        if (layoutCoordinate == null) {
            return null;
        }

        ILayer childLayer = this.childLayerLayout[layoutCoordinate.x][layoutCoordinate.y];
        return childLayer.getCellPainter(
                compositeColumnPosition - getColumnPositionOffset(layoutCoordinate.x),
                compositeRowPosition - getRowPositionOffset(layoutCoordinate.y),
                cell, configRegistry);
    }

    // Child layer stuff

    public void setChildLayer(String regionName, ILayer childLayer,
            final int layoutX, final int layoutY) {
        if (childLayer == null) {
            throw new IllegalArgumentException("Cannot set null child layer"); //$NON-NLS-1$
        }

        this.childLayerToRegionNameMap.put(childLayer, regionName);
        this.regionNameToChildLayerMap.put(regionName, childLayer);

        childLayer.addLayerListener(this);
        this.childLayerLayout[layoutX][layoutY] = childLayer;

        childLayer.setClientAreaProvider(() -> getChildClientArea(layoutX, layoutY));
    }

    public IConfigLabelAccumulator getConfigLabelAccumulatorByRegionName(String regionName) {
        return this.regionNameToConfigLabelAccumulatorMap.get(regionName);
    }

    /**
     * Sets the IConfigLabelAccumulator for the given named region. Replaces any
     * existing IConfigLabelAccumulator.
     *
     * @param regionName
     *            the region name.
     * @param configLabelAccumulator
     *            the {@link IConfigLabelAccumulator} to set.
     */
    public void setConfigLabelAccumulatorForRegion(
            String regionName, IConfigLabelAccumulator configLabelAccumulator) {
        this.regionNameToConfigLabelAccumulatorMap.put(regionName, configLabelAccumulator);
    }

    /**
     * Adds the configLabelAccumulator to the existing label accumulators.
     *
     * @param regionName
     *            the region name.
     * @param configLabelAccumulator
     *            the {@link IConfigLabelAccumulator} to add.
     */
    public void addConfigLabelAccumulatorForRegion(
            String regionName, IConfigLabelAccumulator configLabelAccumulator) {
        IConfigLabelAccumulator existingConfigLabelAccumulator =
                this.regionNameToConfigLabelAccumulatorMap.get(regionName);
        AggregateConfigLabelAccumulator aggregateAccumulator;
        if (existingConfigLabelAccumulator instanceof AggregateConfigLabelAccumulator) {
            aggregateAccumulator = (AggregateConfigLabelAccumulator) existingConfigLabelAccumulator;
        } else {
            aggregateAccumulator = new AggregateConfigLabelAccumulator();
            if (existingConfigLabelAccumulator != null) {
                aggregateAccumulator.add(existingConfigLabelAccumulator);
            }
            this.regionNameToConfigLabelAccumulatorMap.put(regionName, aggregateAccumulator);
        }
        aggregateAccumulator.add(configLabelAccumulator);
    }

    private Rectangle getChildClientArea(final int layoutX, final int layoutY) {
        final ILayer childLayer = this.childLayerLayout[layoutX][layoutY];

        final Rectangle compositeClientArea = getClientAreaProvider().getClientArea();

        if (childLayer.isDynamicSizeLayer()) {

            // check if there are further sections to the bottom and reduce the
            // height accordingly necessary in case the current child layer is
            // a viewport layer that takes the whole space
            if (this.childLayerLayout[layoutX].length > layoutY + 1) {
                for (int bottomY = (layoutY + 1); bottomY < this.childLayerLayout[layoutX].length; bottomY++) {
                    ILayer bottomChildLayer = this.childLayerLayout[layoutX][bottomY];
                    int bottomHeight = bottomChildLayer.getPreferredHeight();
                    // if the layer to the bottom is bigger than the client area
                    // we assume it is the layer that takes the remaining space
                    // e.g. ViewportLayer
                    if (bottomHeight < compositeClientArea.height) {
                        compositeClientArea.height -= bottomHeight;
                    }
                }
            }

            // check if there are further sections to the right and reduce the
            // width accordingly necessary in case the current child layer is
            // a viewport layer that takes the whole space
            if (this.childLayerLayout.length > layoutX + 1) {
                for (int rightX = (layoutX + 1); rightX < this.childLayerLayout.length; rightX++) {
                    ILayer rightChildLayer = this.childLayerLayout[rightX][layoutY];
                    int rightWidth = rightChildLayer.getPreferredWidth();
                    // if the layer to the right is bigger than the client area
                    // we assume it is the layer that takes the remaining space
                    // e.g. ViewportLayer
                    if (rightWidth < compositeClientArea.width) {
                        compositeClientArea.width -= rightWidth;
                    }
                }
            }

        }

        final Rectangle childClientArea = new Rectangle(
                compositeClientArea.x + getWidthOffset(layoutX),
                compositeClientArea.y + getHeightOffset(layoutY),
                childLayer.getPreferredWidth(),
                childLayer.getPreferredHeight());

        return compositeClientArea.intersection(childClientArea);
    }

    /**
     * @param layoutX
     *            col position in the CompositeLayer
     * @param layoutY
     *            row position in the CompositeLayer
     * @return child layer according to the Composite Layer Layout
     */
    public ILayer getChildLayerByLayoutCoordinate(int layoutX, int layoutY) {
        if (layoutX < 0
                || layoutX >= this.layoutXCount
                || layoutY < 0
                || layoutY >= this.layoutYCount) {
            return null;
        } else {
            return this.childLayerLayout[layoutX][layoutY];
        }
    }

    /**
     * @param regionName
     *            The region name for which the child layer is requested.
     * @return The child layer that is registered for the given region name.
     */
    public ILayer getChildLayerByRegionName(String regionName) {
        return this.regionNameToChildLayerMap.get(regionName);
    }

    /**
     * @param x
     *            pixel position
     * @param y
     *            pixel position
     * @return Region which the given position is in
     */
    @Override
    public LabelStack getRegionLabelsByXY(int x, int y) {
        Point layoutCoordinate = getLayoutXYByPixelXY(x, y);
        if (layoutCoordinate == null) {
            return null;
        }

        ILayer childLayer = this.childLayerLayout[layoutCoordinate.x][layoutCoordinate.y];
        int childX = x - getWidthOffset(layoutCoordinate.x);
        int childY = y - getHeightOffset(layoutCoordinate.y);
        LabelStack regionLabels = childLayer.getRegionLabelsByXY(childX, childY);

        String regionName = this.childLayerToRegionNameMap.get(childLayer);
        regionLabels.addLabel(regionName);

        return regionLabels;
    }

    @Override
    public ILayer getUnderlyingLayerByPosition(int columnPosition, int rowPosition) {
        Point layoutCoordinate = getLayoutXYByPosition(columnPosition, rowPosition);
        if (layoutCoordinate != null) {
            return this.childLayerLayout[layoutCoordinate.x][layoutCoordinate.y];
        }
        return null;
    }

    // Layout coordinate accessors

    protected Point getLayoutXYByChildLayer(ILayer childLayer) {
        for (int layoutX = 0; layoutX < this.layoutXCount; layoutX++) {
            for (int layoutY = 0; layoutY < this.layoutYCount; layoutY++) {
                if (this.childLayerLayout[layoutX][layoutY] == childLayer) {
                    return new Point(layoutX, layoutY);
                }
            }
        }
        return null;
    }

    protected int getLayoutXByPixelX(int x) {
        int layoutX = 0;
        while (layoutX < this.layoutXCount) {
            ILayer childLayer = this.childLayerLayout[layoutX][0];
            if (childLayer == null) {
                return -1;
            }
            int widthOffset = getWidthOffset(layoutX);
            if (x >= widthOffset && x < widthOffset + childLayer.getWidth()) {
                return layoutX;
            }

            layoutX++;
        }

        return -1;
    }

    protected int getLayoutYByPixelY(int y) {
        int layoutY = 0;
        while (layoutY < this.layoutYCount) {
            ILayer childLayer = getChildLayerByLayoutCoordinate(0, layoutY);
            if (childLayer == null) {
                return -1;
            }
            int heightOffset = getHeightOffset(layoutY);
            if (y >= heightOffset && y < heightOffset + childLayer.getHeight()) {
                return layoutY;
            }

            layoutY++;
        }

        return -1;
    }

    protected Point getLayoutXYByPixelXY(int x, int y) {
        int layoutX = 0;
        while (layoutX < this.layoutXCount) {
            ILayer childLayer = this.childLayerLayout[layoutX][0];
            if (childLayer == null) {
                return null;
            }
            int widthOffset = getWidthOffset(layoutX);
            if (x >= widthOffset && x < widthOffset + childLayer.getWidth()) {
                break;
            }

            layoutX++;
        }

        int layoutY = 0;
        while (layoutY < this.layoutYCount) {
            ILayer childLayer = getChildLayerByLayoutCoordinate(layoutX, layoutY);
            if (childLayer == null) {
                return null;
            }
            int heightOffset = getHeightOffset(layoutY);
            if (y >= heightOffset && y < heightOffset + childLayer.getHeight()) {
                return new Point(layoutX, layoutY);
            }

            layoutY++;
        }

        return null;
    }

    protected int getLayoutXByColumnPosition(int compositeColumnPosition) {
        int layoutX = 0;
        while (layoutX < this.layoutXCount) {
            ILayer childLayer = this.childLayerLayout[layoutX][0];
            int columnPositionOffset = getColumnPositionOffset(layoutX);
            if (compositeColumnPosition >= columnPositionOffset
                    && compositeColumnPosition < columnPositionOffset + childLayer.getColumnCount()) {
                return layoutX;
            }

            layoutX++;
        }
        return -1;
    }

    protected int getLayoutYByRowPosition(int compositeRowPosition) {
        int layoutY = 0;
        while (layoutY < this.layoutYCount) {
            ILayer childLayer = this.childLayerLayout[0][layoutY];
            int rowPositionOffset = getRowPositionOffset(layoutY);
            if (compositeRowPosition >= rowPositionOffset
                    && compositeRowPosition < rowPositionOffset + childLayer.getRowCount()) {
                return layoutY;
            }

            layoutY++;
        }

        return -1;
    }

    protected Point getLayoutXYByPosition(int compositeColumnPosition,
            int compositeRowPosition) {
        int layoutX = 0;
        while (layoutX < this.layoutXCount) {
            ILayer childLayer = this.childLayerLayout[layoutX][0];
            int columnPositionOffset = getColumnPositionOffset(layoutX);
            if (compositeColumnPosition >= columnPositionOffset
                    && compositeColumnPosition < columnPositionOffset + childLayer.getColumnCount()) {
                break;
            }

            layoutX++;
        }

        if (layoutX >= this.layoutXCount) {
            return null;
        }

        int layoutY = 0;
        while (layoutY < this.layoutYCount) {
            ILayer childLayer = this.childLayerLayout[layoutX][layoutY];
            int rowPositionOffset = getRowPositionOffset(layoutY);
            if (compositeRowPosition >= rowPositionOffset
                    && compositeRowPosition < rowPositionOffset + childLayer.getRowCount()) {
                return new Point(layoutX, layoutY);
            }

            layoutY++;
        }

        return null;
    }

    // Offsets

    protected int getColumnPositionOffset(int layoutX) {
        int offset = 0;
        for (int x = 0; x < layoutX; x++) {
            offset += this.childLayerLayout[x][0].getColumnCount();
        }
        return offset;
    }

    protected int getWidthOffset(int layoutX) {
        int offset = 0;
        for (int x = 0; x < layoutX; x++) {
            offset += this.childLayerLayout[x][0].getWidth();
        }
        return offset;
    }

    protected int getRowPositionOffset(int layoutY) {
        int offset = 0;
        for (int y = 0; y < layoutY; y++) {
            offset += this.childLayerLayout[0][y].getRowCount();
        }
        return offset;
    }

    protected int getHeightOffset(int layoutY) {
        int offset = 0;
        for (int y = 0; y < layoutY; y++) {
            offset += this.childLayerLayout[0][y].getHeight();
        }
        return offset;
    }

    /**
     * @return The number of column regions in this CompositeLayer.
     */
    public int getLayoutXCount() {
        return this.layoutXCount;
    }

    /**
     * @return The number of row regions in this CompositeLayer.
     */
    public int getLayoutYCount() {
        return this.layoutYCount;
    }

    /**
     *
     * @return The internal data structure of the layer composition.
     * @since 1.6
     */
    protected ILayer[][] getChildLayerLayout() {
        return this.childLayerLayout;
    }

    @Override
    public boolean isDynamicSizeLayer() {
        for (int layoutX = 0; layoutX < this.layoutXCount; layoutX++) {
            for (int layoutY = 0; layoutY < this.layoutYCount; layoutY++) {
                if (this.childLayerLayout[layoutX][layoutY].isDynamicSizeLayer()) {
                    return true;
                }
            }
        }
        return false;
    }

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

        for (int layoutX = 0; layoutX < this.layoutXCount; layoutX++) {
            for (int layoutY = 0; layoutY < this.layoutYCount; layoutY++) {
                ILayer childLayer = this.childLayerLayout[layoutX][layoutY];
                String regionName = this.childLayerToRegionNameMap.get(childLayer);
                labels.add(regionName);

                IConfigLabelAccumulator accumulator = this.regionNameToConfigLabelAccumulatorMap.get(regionName);
                if (accumulator instanceof IConfigLabelProvider) {
                    labels.addAll(((IConfigLabelProvider) accumulator).getProvidedLabels());
                }

                labels.addAll(childLayer.getProvidedLabels());
            }
        }
        return labels;
    }

    /**
     * The {@link ILayerPainter} that is used to render compositions.
     *
     * @since 1.6
     */
    public class CompositeLayerPainter implements ILayerPainter {

        @Override
        public void paintLayer(ILayer natLayer, GC gc, int xOffset,
                int yOffset, Rectangle rectangle, IConfigRegistry configuration) {
            int x = xOffset;
            for (int layoutX = 0; layoutX < CompositeLayer.this.layoutXCount; layoutX++) {
                int y = yOffset;
                for (int layoutY = 0; layoutY < CompositeLayer.this.layoutYCount; layoutY++) {
                    ILayer childLayer = CompositeLayer.this.childLayerLayout[layoutX][layoutY];

                    Rectangle childLayerRectangle = new Rectangle(
                            x, y, childLayer.getWidth(), childLayer.getHeight());

                    childLayerRectangle = rectangle.intersection(childLayerRectangle);

                    Rectangle originalClipping = gc.getClipping();
                    gc.setClipping(childLayerRectangle);

                    childLayer.getLayerPainter().paintLayer(
                            natLayer, gc, x, y, childLayerRectangle, configuration);

                    gc.setClipping(originalClipping);
                    y += childLayer.getHeight();
                }

                x += CompositeLayer.this.childLayerLayout[layoutX][0].getWidth();
            }
        }

        @Override
        public Rectangle adjustCellBounds(int columnPosition, int rowPosition, Rectangle cellBounds) {
            Point layoutCoordinate = getLayoutXYByPosition(columnPosition, rowPosition);
            if (layoutCoordinate == null) {
                return null;
            }

            ILayer childLayer = CompositeLayer.this.childLayerLayout[layoutCoordinate.x][layoutCoordinate.y];
            if (childLayer == null) {
                return null;
            }

            int widthOffset = getWidthOffset(layoutCoordinate.x);
            int heightOffset = getHeightOffset(layoutCoordinate.y);

            cellBounds.x -= widthOffset;
            cellBounds.y -= heightOffset;

            ILayerPainter childLayerPainter = childLayer.getLayerPainter();
            int childColumnPosition = columnPosition - getColumnPositionOffset(layoutCoordinate.x);
            int childRowPosition = rowPosition - getRowPositionOffset(layoutCoordinate.y);
            Rectangle adjustedChildCellBounds = childLayerPainter.adjustCellBounds(
                    childColumnPosition, childRowPosition, cellBounds);

            adjustedChildCellBounds.x += widthOffset;
            adjustedChildCellBounds.y += heightOffset;

            return adjustedChildCellBounds;
        }

    }

}
