/*******************************************************************************
 * 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> - Added scaling
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.group;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Properties;

import org.eclipse.nebula.widgets.nattable.NatTable;
import org.eclipse.nebula.widgets.nattable.grid.GridRegion;
import org.eclipse.nebula.widgets.nattable.group.config.DefaultRowGroupHeaderLayerConfiguration;
import org.eclipse.nebula.widgets.nattable.group.model.IRowGroup;
import org.eclipse.nebula.widgets.nattable.group.model.IRowGroupModel;
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.LabelStack;
import org.eclipse.nebula.widgets.nattable.layer.SizeConfig;
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.RowStructuralRefreshEvent;
import org.eclipse.nebula.widgets.nattable.selection.SelectRowGroupCommandHandler;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer;
import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
import org.eclipse.nebula.widgets.nattable.style.SelectionStyleLabels;

/**
 * Adds the Row grouping functionality to the row headers. Also persists the
 * state of the row groups when {@link NatTable#saveState(String, Properties)}
 * is invoked.
 *
 * Internally uses the {@link IRowGroupModel} to track the row groups.
 */
public class RowGroupHeaderLayer<T> extends AbstractLayerTransform {

    private final SizeConfig columnWidthConfig = new SizeConfig(DataLayer.DEFAULT_COLUMN_WIDTH);
    private final IRowGroupModel<T> model;
    private final SelectionLayer selectionLayer;
    private final ILayer rowHeaderLayer;

    public RowGroupHeaderLayer(
            ILayer rowHeaderLayer,
            SelectionLayer selectionLayer,
            IRowGroupModel<T> rowGroupModel) {

        this(rowHeaderLayer, selectionLayer, rowGroupModel, true);
    }

    public RowGroupHeaderLayer(
            ILayer rowHeaderLayer,
            SelectionLayer selectionLayer,
            IRowGroupModel<T> rowGroupModel,
            boolean useDefaultConfiguration) {

        super(rowHeaderLayer);
        this.rowHeaderLayer = rowHeaderLayer;
        this.selectionLayer = selectionLayer;
        this.model = rowGroupModel;

        registerCommandHandlers();

        if (useDefaultConfiguration) {
            addConfiguration(new DefaultRowGroupHeaderLayerConfiguration());
        }
    }

    public IRowGroupModel<T> getModel() {
        return this.model;
    }

    // Persistence

    @Override
    public void loadState(String prefix, Properties properties) {
        super.loadState(prefix, properties);
        this.model.loadState(prefix, properties);
        fireLayerEvent(new RowStructuralRefreshEvent(this));
    }

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

    // Configuration

    @Override
    protected void registerCommandHandlers() {
        registerCommandHandler(new SelectRowGroupCommandHandler<>(this.model, this.selectionLayer, this));
        registerCommandHandler(new ConfigureScalingCommandHandler(this.columnWidthConfig, null));
    }

    // Horizontal features

    // Columns

    @Override
    public int getColumnCount() {
        return this.rowHeaderLayer.getColumnCount() + 1;
    }

    @Override
    public int getPreferredColumnCount() {
        return this.rowHeaderLayer.getPreferredColumnCount() + 1;
    }

    @Override
    public int getColumnIndexByPosition(int columnPosition) {
        if (columnPosition == 0) {
            return columnPosition;
        } else {
            return this.rowHeaderLayer.getColumnIndexByPosition(columnPosition - 1);
        }
    }

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

    // Width

    @Override
    public int getWidth() {
        return this.columnWidthConfig.getAggregateSize(1)
                + this.rowHeaderLayer.getWidth();
    }

    @Override
    public int getPreferredWidth() {
        return this.columnWidthConfig.getAggregateSize(1)
                + this.rowHeaderLayer.getPreferredWidth();
    }

    @Override
    public int getColumnWidthByPosition(int columnPosition) {
        if (columnPosition == 0) {
            return this.columnWidthConfig.getSize(columnPosition);
        } else {
            return this.rowHeaderLayer.getColumnWidthByPosition(columnPosition - 1);
        }
    }

    public void setColumnWidth(int columnWidth) {
        this.columnWidthConfig.setSize(0, columnWidth);
    }

    // Column resize

    @Override
    public boolean isColumnPositionResizable(int columnPosition) {
        if (columnPosition == 0) {
            return this.columnWidthConfig.isPositionResizable(columnPosition);
        } else {
            return this.rowHeaderLayer.isRowPositionResizable(columnPosition - 1);
        }
    }

    // X

    @Override
    public int getColumnPositionByX(int x) {
        int col0Width = getColumnWidthByPosition(0);
        if (x < col0Width) {
            return 0;
        } else {
            return 1 + this.rowHeaderLayer.getColumnPositionByX(x - col0Width);
        }
    }

    @Override
    public int getStartXOfColumnPosition(int columnPosition) {
        if (columnPosition == 0) {
            return this.columnWidthConfig.getAggregateSize(columnPosition);
        } else {
            return getColumnWidthByPosition(0)
                    + this.rowHeaderLayer.getStartXOfColumnPosition(columnPosition - 1);
        }
    }

    // Cell features

    /**
     * If a cell belongs to a column group: column position - set to the start
     * position of the group span - set to the width/size of the row group
     *
     * NOTE: gc.setClip() is used in the CompositeLayerPainter to ensure that
     * partially visible Column group header cells are rendered properly.
     */
    @Override
    public ILayerCell getCellByPosition(int columnPosition, int rowPosition) {
        int bodyRowIndex = getRowIndexByPosition(rowPosition);

        // Row group header cell
        if (RowGroupUtils.isPartOfAGroup(this.model, bodyRowIndex)) {
            if (columnPosition == 0) {
                return new LayerCell(
                        this,
                        columnPosition,
                        getStartPositionOfGroup(rowPosition),
                        columnPosition,
                        rowPosition,
                        1,
                        getRowSpan(rowPosition));
            } else {
                return new LayerCell(this, columnPosition, rowPosition);
            }
        } else {
            // render row header w/ columnspan = 2
            // as in this case we ask the row header layer for the cell position
            // and the row header layer asks his data provider for the column
            // count which should always return 1, we ask for row position 0
            ILayerCell cell = this.rowHeaderLayer.getCellByPosition(0, rowPosition);
            if (cell != null) {
                cell = new TransformedLayerCell(cell) {
                    @Override
                    public ILayer getLayer() {
                        return RowGroupHeaderLayer.this;
                    }

                    @Override
                    public int getColumnSpan() {
                        return 2;
                    }
                };
            }
            return cell;
        }
    }

    /**
     * Calculates the span of a cell in a Row Group. Takes into account
     * collapsing and hidden rows in the group.
     *
     * @param rowPosition
     *            position of any row belonging to the group
     */
    protected int getRowSpan(int rowPosition) {
        int rowIndex = getRowIndexByPosition(rowPosition);

        // Get the row and the group from our cache and model.
        IRowGroup<T> rowGroup = RowGroupUtils.getRowGroupForRowIndex(this.model, rowIndex);

        int sizeOfGroup = RowGroupUtils.sizeOfGroup(this.model, rowIndex);

        if (RowGroupUtils.isCollapsed(this.model, rowGroup)) {
            int sizeOfStaticRows = rowGroup.getOwnStaticMemberRows().size();
            if (sizeOfStaticRows == 0) {
                return 1;
            } else {
                sizeOfGroup = sizeOfStaticRows;
            }
        }

        int startPositionOfGroup = getStartPositionOfGroup(rowPosition);
        int endPositionOfGroup = startPositionOfGroup + sizeOfGroup;
        List<Integer> rowIndexesInGroup = RowGroupUtils.getRowIndexesInGroup(this.model, rowIndex);

        for (int i = startPositionOfGroup; i < endPositionOfGroup; i++) {
            int index = getRowIndexByPosition(i);
            if (!rowIndexesInGroup.contains(Integer.valueOf(index))) {
                sizeOfGroup--;
            }
        }

        return sizeOfGroup;
    }

    /**
     * Figures out the start position of the group.
     *
     * @param selectionLayerColumnPosition
     *            of any column belonging to the group
     * @return first position of the column group
     */
    private int getStartPositionOfGroup(int rowPosition) {
        int bodyRowIndex = getRowIndexByPosition(rowPosition);
        int leastPossibleStartPositionOfGroup =
                Math.max(0, (rowPosition - RowGroupUtils.sizeOfGroup(this.model, bodyRowIndex)));
        int i = 0;
        for (i = leastPossibleStartPositionOfGroup; i < rowPosition; i++) {
            if (RowGroupUtils.isInTheSameGroup(
                    getRowIndexByPosition(i),
                    bodyRowIndex,
                    this.model)) {
                break;
            }
        }
        return i;
    }

    @Override
    public DisplayMode getDisplayModeByPosition(int columnPosition, int rowPosition) {
        int rowIndex = getRowIndexByPosition(rowPosition);
        if (columnPosition == 0
                && RowGroupUtils.isPartOfAGroup(this.model, rowIndex)) {
            return DisplayMode.NORMAL;
        } else {
            return this.rowHeaderLayer.getDisplayModeByPosition(columnPosition - 1, rowPosition);
        }
    }

    @Override
    public LabelStack getConfigLabelsByPosition(int columnPosition, int rowPosition) {
        int rowIndex = getRowIndexByPosition(rowPosition);
        if (columnPosition == 0 && RowGroupUtils.isPartOfAGroup(this.model, rowIndex)) {
            LabelStack stack = new LabelStack();
            if (getConfigLabelAccumulator() != null) {
                getConfigLabelAccumulator().accumulateConfigLabels(stack, columnPosition, rowPosition);
            }
            stack.addLabel(GridRegion.ROW_GROUP_HEADER);

            IRowGroup<T> group = RowGroupUtils.getRowGroupForRowIndex(this.model, rowIndex);
            if (RowGroupUtils.isCollapsed(this.model, group)) {
                stack.addLabelOnTop(DefaultRowGroupHeaderLayerConfiguration.GROUP_COLLAPSED_CONFIG_TYPE);
            } else {
                stack.addLabelOnTop(DefaultRowGroupHeaderLayerConfiguration.GROUP_EXPANDED_CONFIG_TYPE);
            }

            List<Integer> selectedRowIndexes =
                    convertToRowIndexes(this.selectionLayer.getFullySelectedRowPositions());
            if (selectedRowIndexes.contains(rowIndex)) {
                stack.addLabelOnTop(SelectionStyleLabels.ROW_FULLY_SELECTED_STYLE);
            }

            return stack;
        } else {
            return this.rowHeaderLayer.getConfigLabelsByPosition(columnPosition - 1, rowPosition);
        }
    }

    private List<Integer> convertToRowIndexes(final int[] rowPositions) {
        final List<Integer> rowIndexes = new ArrayList<>(rowPositions.length);
        for (final Integer rowPosition : rowPositions) {
            rowIndexes.add(this.selectionLayer.getRowIndexByPosition(rowPosition));
        }
        return rowIndexes;
    }

    @Override
    public Object getDataValueByPosition(int columnPosition, int rowPosition) {
        int rowIndex = getRowIndexByPosition(rowPosition);
        if (columnPosition == 0
                && RowGroupUtils.isPartOfAGroup(this.model, rowIndex)) {
            return RowGroupUtils.getRowGroupNameForIndex(this.model, rowIndex);
        } else {
            return this.rowHeaderLayer.getDataValueByPosition(columnPosition - 1, rowPosition);
        }
    }

    @Override
    public LabelStack getRegionLabelsByXY(int x, int y) {
        int rowIndex = getRowIndexByPosition(getRowPositionByY(y));
        if (RowGroupUtils.isPartOfAGroup(this.model, rowIndex)
                && x < getColumnWidthByPosition(0)) {
            return new LabelStack(GridRegion.ROW_GROUP_HEADER);
        } else {
            return this.rowHeaderLayer.getRegionLabelsByXY(x - getColumnWidthByPosition(0), y);
        }
    }

    public void collapseRowGroupByIndex(int rowIndex) {
        RowGroupUtils.getRowGroupForRowIndex(this.model, rowIndex).collapse();
    }

    public void clearAllGroups() {
        this.model.clear();
    }

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

        labels.add(GridRegion.ROW_GROUP_HEADER);
        labels.add(DefaultRowGroupHeaderLayerConfiguration.GROUP_COLLAPSED_CONFIG_TYPE);
        labels.add(DefaultRowGroupHeaderLayerConfiguration.GROUP_EXPANDED_CONFIG_TYPE);

        return labels;
    }
}
