/*******************************************************************************
 * 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
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.grid.layer;

import java.util.Collection;
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.AbstractLayer;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
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.cell.IConfigLabelAccumulator;
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.nebula.widgets.nattable.util.IClientAreaProvider;

/**
 * <p>
 * A DimensionallyDependentLayer is a layer whose horizontal and vertical
 * dimensions are dependent on the horizontal and vertical dimensions of other
 * layers. A DimensionallyDependentLayer takes three constructor parameters: the
 * horizontal layer that the DimensionallyDependentLayer's horizontal dimension
 * is linked to, the vertical layer that the DimensionallyDependentLayer is
 * linked to, and a base layer to which all non-dimensionally related ILayer
 * method calls will be delegated to (e.g. command, event methods)
 * </p>
 * <p>
 * Prime examples of dimensionally dependent layers are the column header and
 * row header layers. For example, the column header layer's horizontal
 * dimension is linked to the body layer's horizontal dimension. This means that
 * whatever columns are shown in the body area will also be shown in the column
 * header area, and vice versa. Note that the column header layer maintains its
 * own vertical dimension, however, so it's vertical layer dependency would be a
 * separate data layer. The same is true for the row header layer, only with the
 * vertical instead of the horizontal dimension. The constructors for the column
 * header and row header layers would therefore look something like this:
 * </p>
 *
 * <pre>
 * ILayer columnHeaderLayer = new DimensionallyDependentLayer(
 *         columnHeaderRowDataLayer, bodyLayer, columnHeaderRowDataLayer);
 * ILayer rowHeaderLayer = new DimensionallyDependentLayer(
 *         rowHeaderColumnDataLayer, bodyLayer, rowHeaderColumnDataLayer);
 * </pre>
 */
public class DimensionallyDependentLayer extends AbstractLayer {

    private final IUniqueIndexLayer baseLayer;
    private ILayer horizontalLayerDependency;
    private ILayer verticalLayerDependency;
    private IClientAreaProvider clientAreaProvider;

    protected DimensionallyDependentLayer(IUniqueIndexLayer baseLayer) {
        this.baseLayer = baseLayer;
        this.baseLayer.addLayerListener(this);
    }

    public DimensionallyDependentLayer(
            IUniqueIndexLayer baseLayer, ILayer horizontalLayerDependency, ILayer verticalLayerDependency) {

        this.baseLayer = baseLayer;
        this.baseLayer.addLayerListener(this);

        setHorizontalLayerDependency(horizontalLayerDependency);
        setVerticalLayerDependency(verticalLayerDependency);
    }

    // Persistence

    @Override
    public void saveState(String prefix, Properties properties) {
        super.saveState(prefix, properties);
        this.baseLayer.saveState(prefix, properties);
    }

    @Override
    public void loadState(String prefix, Properties properties) {
        super.loadState(prefix, properties);
        this.baseLayer.loadState(prefix, properties);
    }

    // Configuration

    @Override
    public void configure(IConfigRegistry configRegistry, UiBindingRegistry uiBindingRegistry) {
        this.baseLayer.configure(configRegistry, uiBindingRegistry);
        super.configure(configRegistry, uiBindingRegistry);
    }

    // Dependent layer accessors

    public void setHorizontalLayerDependency(ILayer horizontalLayerDependency) {
        this.horizontalLayerDependency = horizontalLayerDependency;

        // this.horizontalLayerDependency.addLayerListener(new ILayerListener()
        // {
        //
        // @Override
        // public void handleLayerEvent(ILayerEvent event) {
        // if (event instanceof IStructuralChangeEvent) {
        // // TODO refresh horizontal structure
        // }
        // }
        //
        // });
    }

    public void setVerticalLayerDependency(ILayer verticalLayerDependency) {
        this.verticalLayerDependency = verticalLayerDependency;

        // this.verticalLayerDependency.addLayerListener(new ILayerListener() {
        //
        // @Override
        // public void handleLayerEvent(ILayerEvent event) {
        // if (event instanceof IStructuralChangeEvent) {
        // // TODO refresh vertical structure
        // }
        // }
        //
        // });
    }

    public ILayer getHorizontalLayerDependency() {
        return this.horizontalLayerDependency;
    }

    public ILayer getVerticalLayerDependency() {
        return this.verticalLayerDependency;
    }

    public IUniqueIndexLayer getBaseLayer() {
        return this.baseLayer;
    }

    // Commands

    @Override
    public boolean doCommand(ILayerCommand command) {
        // Invoke command handler(s) on the Dimensionally dependent layer
        ILayerCommand clonedCommand = command.cloneCommand();
        if (super.doCommand(clonedCommand)) {
            return true;
        }

        // in case we have a command for a specific region we need to ensure
        // that no other regions are affected
        if (!(command instanceof AbstractRegionCommand)) {
            clonedCommand = command.cloneCommand();
            if (this.horizontalLayerDependency.doCommand(clonedCommand)) {
                return true;
            }

            clonedCommand = command.cloneCommand();
            if (this.verticalLayerDependency.doCommand(clonedCommand)) {
                return true;
            }
        }

        return this.baseLayer.doCommand(command);
    }

    // Events

    @Override
    public ILayerPainter getLayerPainter() {
        return (this.layerPainter != null) ? this.layerPainter : this.baseLayer.getLayerPainter();
    }

    // Horizontal features

    // Columns

    @Override
    public int getColumnCount() {
        return this.horizontalLayerDependency.getColumnCount();
    }

    @Override
    public int getPreferredColumnCount() {
        return this.horizontalLayerDependency.getPreferredColumnCount();
    }

    @Override
    public int getColumnIndexByPosition(int columnPosition) {
        return this.horizontalLayerDependency.getColumnIndexByPosition(columnPosition);
    }

    @Override
    public int localToUnderlyingColumnPosition(int localColumnPosition) {
        return this.horizontalLayerDependency.localToUnderlyingColumnPosition(localColumnPosition);
    }

    @Override
    public int underlyingToLocalColumnPosition(ILayer sourceUnderlyingLayer, int underlyingColumnPosition) {
        if (sourceUnderlyingLayer == this.horizontalLayerDependency) {
            return underlyingColumnPosition;
        }
        return this.horizontalLayerDependency
                .underlyingToLocalColumnPosition(sourceUnderlyingLayer, underlyingColumnPosition);
    }

    @Override
    public Collection<Range> underlyingToLocalColumnPositions(
            ILayer sourceUnderlyingLayer,
            Collection<Range> underlyingColumnPositionRanges) {

        if (sourceUnderlyingLayer == this.horizontalLayerDependency) {
            return underlyingColumnPositionRanges;
        }
        return this.horizontalLayerDependency
                .underlyingToLocalColumnPositions(sourceUnderlyingLayer, underlyingColumnPositionRanges);
    }

    // Width

    @Override
    public int getWidth() {
        return this.horizontalLayerDependency.getWidth();
    }

    @Override
    public int getPreferredWidth() {
        return this.horizontalLayerDependency.getPreferredWidth();
    }

    @Override
    public int getColumnWidthByPosition(int columnPosition) {
        return this.horizontalLayerDependency.getColumnWidthByPosition(columnPosition);
    }

    // Column resize

    @Override
    public boolean isColumnPositionResizable(int columnPosition) {
        return this.horizontalLayerDependency.isColumnPositionResizable(columnPosition);
    }

    // X

    @Override
    public int getColumnPositionByX(int x) {
        return this.horizontalLayerDependency.getColumnPositionByX(x);
    }

    @Override
    public int getStartXOfColumnPosition(int columnPosition) {
        return this.horizontalLayerDependency.getStartXOfColumnPosition(columnPosition);
    }

    // Underlying

    @Override
    public Collection<ILayer> getUnderlyingLayersByColumnPosition(int columnPosition) {
        Collection<ILayer> underlyingLayers = new HashSet<>();
        underlyingLayers.add(this.baseLayer);
        return underlyingLayers;
    }

    // Vertical features

    // Rows

    @Override
    public int getRowCount() {
        return this.verticalLayerDependency.getRowCount();
    }

    @Override
    public int getPreferredRowCount() {
        return this.verticalLayerDependency.getPreferredRowCount();
    }

    @Override
    public int getRowIndexByPosition(int rowPosition) {
        return this.verticalLayerDependency.getRowIndexByPosition(rowPosition);
    }

    @Override
    public int localToUnderlyingRowPosition(int localRowPosition) {
        return this.verticalLayerDependency.localToUnderlyingRowPosition(localRowPosition);
    }

    @Override
    public int underlyingToLocalRowPosition(
            ILayer sourceUnderlyingLayer, int underlyingRowPosition) {

        if (sourceUnderlyingLayer == this.verticalLayerDependency) {
            return underlyingRowPosition;
        }
        return this.verticalLayerDependency
                .underlyingToLocalRowPosition(sourceUnderlyingLayer, underlyingRowPosition);
    }

    @Override
    public Collection<Range> underlyingToLocalRowPositions(
            ILayer sourceUnderlyingLayer, Collection<Range> underlyingRowPositionRanges) {

        if (sourceUnderlyingLayer == this.verticalLayerDependency) {
            return underlyingRowPositionRanges;
        }
        return this.verticalLayerDependency
                .underlyingToLocalRowPositions(sourceUnderlyingLayer, underlyingRowPositionRanges);
    }

    // Height

    @Override
    public int getHeight() {
        return this.verticalLayerDependency.getHeight();
    }

    @Override
    public int getPreferredHeight() {
        return this.verticalLayerDependency.getPreferredHeight();
    }

    @Override
    public int getRowHeightByPosition(int rowPosition) {
        return this.verticalLayerDependency.getRowHeightByPosition(rowPosition);
    }

    // Row resize

    @Override
    public boolean isRowPositionResizable(int rowPosition) {
        return this.verticalLayerDependency.isRowPositionResizable(rowPosition);
    }

    // Y

    @Override
    public int getRowPositionByY(int y) {
        return this.verticalLayerDependency.getRowPositionByY(y);
    }

    @Override
    public int getStartYOfRowPosition(int rowPosition) {
        return this.verticalLayerDependency.getStartYOfRowPosition(rowPosition);
    }

    // Underlying

    @Override
    public Collection<ILayer> getUnderlyingLayersByRowPosition(int rowPosition) {
        Collection<ILayer> underlyingLayers = new HashSet<>();
        underlyingLayers.add(this.baseLayer);
        return underlyingLayers;
    }

    // Cell features

    @Override
    public DisplayMode getDisplayModeByPosition(int columnPosition, int rowPosition) {
        int baseColumnPosition = LayerUtil.convertColumnPosition(this, columnPosition, this.baseLayer);
        int baseRowPosition = LayerUtil.convertRowPosition(this, rowPosition, this.baseLayer);
        return this.baseLayer.getDisplayModeByPosition(baseColumnPosition, baseRowPosition);
    }

    @Override
    public LabelStack getConfigLabelsByPosition(int columnPosition, int rowPosition) {
        int baseColumnPosition = LayerUtil.convertColumnPosition(this, columnPosition, this.baseLayer);
        int baseRowPosition = LayerUtil.convertRowPosition(this, rowPosition, this.baseLayer);
        LabelStack labelStack = this.baseLayer.getConfigLabelsByPosition(baseColumnPosition, baseRowPosition);

        IConfigLabelAccumulator configLabelAccumulator = getConfigLabelAccumulator();
        if (configLabelAccumulator != null) {
            configLabelAccumulator.accumulateConfigLabels(labelStack, columnPosition, rowPosition);
        }

        return labelStack;
    }

    @Override
    public Object getDataValueByPosition(int columnPosition, int rowPosition) {
        int baseColumnPosition = LayerUtil.convertColumnPosition(this, columnPosition, this.baseLayer);
        int baseRowPosition = LayerUtil.convertRowPosition(this, rowPosition, this.baseLayer);
        return this.baseLayer.getDataValueByPosition(baseColumnPosition, baseRowPosition);
    }

    // IRegionResolver

    @Override
    public LabelStack getRegionLabelsByXY(int x, int y) {
        return this.baseLayer.getRegionLabelsByXY(x, y);
    }

    @Override
    public IClientAreaProvider getClientAreaProvider() {
        return this.clientAreaProvider;
    }

    @Override
    public void setClientAreaProvider(IClientAreaProvider clientAreaProvider) {
        this.clientAreaProvider = clientAreaProvider;
    }

    @Override
    public ILayer getUnderlyingLayerByPosition(int columnPosition, int rowPosition) {
        return this.baseLayer;
    }

}
