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

import java.util.Collection;

import org.eclipse.nebula.widgets.nattable.command.DisposeResourcesCommand;
import org.eclipse.nebula.widgets.nattable.command.ILayerCommand;
import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.layer.AbstractLayerTransform;
import org.eclipse.nebula.widgets.nattable.layer.DataLayer;
import org.eclipse.nebula.widgets.nattable.layer.IDpiConverter;
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.ILayerCell;
import org.eclipse.nebula.widgets.nattable.layer.cell.LayerCell;
import org.eclipse.nebula.widgets.nattable.layer.command.ConfigureScalingCommand;
import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.IVisualChangeEvent;
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.style.DisplayMode;
import org.eclipse.nebula.widgets.nattable.summaryrow.command.CalculateSummaryRowValuesCommand;
import org.eclipse.nebula.widgets.nattable.util.ArrayUtil;
import org.eclipse.nebula.widgets.nattable.util.CalculatedValueCache;
import org.eclipse.nebula.widgets.nattable.util.ICalculatedValueCache;

/**
 * Adds a summary row at the end. Uses {@link ISummaryProvider} to calculate the
 * summaries for all columns.
 * <p>
 * This layer also adds the following labels:
 * <ol>
 * <li>{@link SummaryRowLayer#DEFAULT_SUMMARY_COLUMN_CONFIG_LABEL_PREFIX} +
 * column index</li>
 * <li>{@link SummaryRowLayer#DEFAULT_SUMMARY_ROW_CONFIG_LABEL} to all cells in
 * the row</li>
 * </ol>
 *
 * Example: column with index 1 will have the
 * DEFAULT_SUMMARY_COLUMN_CONFIG_LABEL_PREFIX + 1 label applied. Styling and
 * {@link ISummaryProvider} can be hooked up to these labels.
 *
 * @see DefaultSummaryRowConfiguration
 */
public class SummaryRowLayer extends AbstractLayerTransform implements IUniqueIndexLayer {

    /**
     * Label that gets attached to the LabelStack for every cell in the summary
     * row.
     */
    public static final String DEFAULT_SUMMARY_ROW_CONFIG_LABEL = "SummaryRow"; //$NON-NLS-1$
    /**
     * Prefix of the labels that get attached to cells in the summary row. The
     * complete label will consist of this prefix and the column index at the
     * end of the label. This way every cell in the summary row can be accessed
     * directly via label mechanism.
     */
    public static final String DEFAULT_SUMMARY_COLUMN_CONFIG_LABEL_PREFIX = "SummaryColumn_"; //$NON-NLS-1$
    /**
     * The ConfigRegistry for retrieving the ISummaryProvider per column.
     */
    private final IConfigRegistry configRegistry;
    /**
     * The row height of the summary row. As the summary row itself is not
     * connected to a DataLayer, this layer needs to store the height for
     * itself.
     * <p>
     * Note: We are not using a SizeConfig here because the position of the
     * summary row might change in case rows are added.
     * </p>
     */
    private int summaryRowHeight = DataLayer.DEFAULT_ROW_HEIGHT;
    /**
     * The value cache that contains the summary values and performs summary
     * calculation in background processes if necessary.
     */
    private ICalculatedValueCache valueCache;
    /**
     * Converter that is used to ensure the row height of the summary row is
     * scaled correctly.
     */
    private IDpiConverter dpiConverter;

    /**
     * Flag to configure whether the summary row should be rendered below an
     * underlying layer or if it should be rendered standalone. Setting this
     * value to <code>true</code> will enable rendering of the single summary
     * row in a composite region.
     */
    private boolean standalone = false;

    /**
     * Creates a SummaryRowLayer on top of the given underlying layer. It uses
     * smooth value updates as default.
     * <p>
     * Note: This constructor will create the SummaryRowLayer by using the
     * default configuration. The default configuration doesn't fit the needs so
     * you usually will use your custom summary row configuration.
     *
     * @param underlyingDataLayer
     *            The underlying layer on which the SummaryRowLayer should be
     *            build.
     * @param configRegistry
     *            The ConfigRegistry for retrieving the ISummaryProvider per
     *            column.
     *
     * @see DefaultSummaryRowConfiguration
     */
    public SummaryRowLayer(IUniqueIndexLayer underlyingDataLayer, IConfigRegistry configRegistry) {
        this(underlyingDataLayer, configRegistry, true, true);
    }

    /**
     * Creates a SummaryRowLayer on top of the given underlying layer. It uses
     * smooth value updates as default.
     * <p>
     * Note: This constructor will create the SummaryRowLayer by using the
     * default configuration if the autoConfig parameter is set to
     * <code>true</code>. The default configuration doesn't fit the needs so you
     * usually will use your custom summary row configuration. When using a
     * custom configuration you should use this constructor setting autoConfig
     * to <code>false</code>. Otherwise you might get strange behaviour as the
     * default configuration will be set additionally to your configuration.
     *
     * @param underlyingDataLayer
     *            The underlying layer on which the SummaryRowLayer should be
     *            build.
     * @param configRegistry
     *            The ConfigRegistry for retrieving the ISummaryProvider per
     *            column.
     * @param autoConfigure
     *            <code>true</code> to use the DefaultSummaryRowConfiguration,
     *            <code>false</code> if a custom configuration will be set after
     *            the creation.
     *
     * @see DefaultSummaryRowConfiguration
     */
    public SummaryRowLayer(IUniqueIndexLayer underlyingDataLayer, IConfigRegistry configRegistry, boolean autoConfigure) {
        this(underlyingDataLayer, configRegistry, true, autoConfigure);
    }

    /**
     * Creates a SummaryRowLayer on top of the given underlying layer.
     * <p>
     * Note: This constructor will create the SummaryRowLayer by using the
     * default configuration if the autoConfig parameter is set to
     * <code>true</code>. The default configuration doesn't fit the needs so you
     * usually will use your custom summary row configuration. When using a
     * custom configuration you should use this constructor setting autoConfig
     * to <code>false</code>. Otherwise you might get strange behaviour as the
     * default configuration will be set additionally to your configuration.
     *
     * @param underlyingDataLayer
     *            The underlying layer on which the SummaryRowLayer should be
     *            build.
     * @param configRegistry
     *            The ConfigRegistry for retrieving the ISummaryProvider per
     *            column.
     * @param smoothUpdates
     *            <code>true</code> if the summary value updates should be
     *            performed smoothly, <code>false</code> if on re-calculation
     *            the value should be immediately shown as not calculated.
     * @param autoConfigure
     *            <code>true</code> to use the DefaultSummaryRowConfiguration,
     *            <code>false</code> if a custom configuration will be set after
     *            the creation.
     *
     * @see DefaultSummaryRowConfiguration
     */
    public SummaryRowLayer(IUniqueIndexLayer underlyingDataLayer, IConfigRegistry configRegistry,
            boolean smoothUpdates, boolean autoConfigure) {

        super(underlyingDataLayer);
        this.configRegistry = configRegistry;

        this.valueCache = new CalculatedValueCache(this, true, false, smoothUpdates);

        if (autoConfigure) {
            addConfiguration(new DefaultSummaryRowConfiguration());
        }
    }

    /**
     * Calculates the summary for the column using the {@link ISummaryProvider}
     * from the {@link IConfigRegistry}. In order to prevent the table from
     * freezing (for large data sets), the summary is calculated in a separate
     * Thread. While summary is being calculated
     * {@link ISummaryProvider#DEFAULT_SUMMARY_VALUE} is returned.
     * <p>
     * NOTE: Since this is a {@link IUniqueIndexLayer} sitting close to the
     * {@link DataLayer}, columnPosition == columnIndex
     */
    @Override
    public Object getDataValueByPosition(final int columnPosition, final int rowPosition) {
        if (isSummaryRowPosition(rowPosition)) {
            return calculateNewSummaryValue(columnPosition, true);
        }
        return super.getDataValueByPosition(columnPosition, rowPosition);
    }

    /**
     * Asks the local CalculatedValueCache for summary value. Is also used to
     * trigger calculation prior printing or exporting to ensure that the values
     * are calculated. This is necessary because usually the calculation will be
     * done when the data value is requested the first time.
     *
     * @param columnPosition
     *            The column position of the requested summary value.
     * @param calculateInBackground
     *            <code>true</code> if the summary value calculation should be
     *            processed in a background thread, <code>false</code> if the
     *            calculation should be processed in the current thread.
     * @return The value that is calculated in the CalculatedValueCache or a
     *         temporary value if the calculation process is started in a
     *         background thread.
     */
    private Object calculateNewSummaryValue(final int columnPosition, boolean calculateInBackground) {

        // as we only care about one row in the value cache, the real row
        // position doesn't matter in fact using the real summary row
        // position would cause issues if rows are added or removed because
        // the value cache takes into account column and row position for
        // caching.
        return this.valueCache.getCalculatedValue(
                columnPosition,
                getSummaryRowPosition(),
                calculateInBackground,
                () -> {
                    LabelStack labelStack = getConfigLabelsByPositionWithoutTransformation(columnPosition, getSummaryRowPosition());
                    String[] configLabels = labelStack.toArray(ArrayUtil.STRING_TYPE_ARRAY);

                    final ISummaryProvider summaryProvider = SummaryRowLayer.this.configRegistry.getConfigAttribute(
                            SummaryRowConfigAttributes.SUMMARY_PROVIDER,
                            DisplayMode.NORMAL,
                            configLabels);

                    // If there is no Summary provider - skip processing
                    if (summaryProvider == ISummaryProvider.NONE || summaryProvider == null) {
                        return null;
                    }

                    return summaryProvider.summarize(columnPosition);
                });
    }

    /**
     * Checks if the given row position is the position of the summary row.
     *
     * @param rowPosition
     *            The row position to check.
     * @return <code>true</code> if the given row position is the summary row
     *         position.
     * @since 1.6
     */
    public boolean isSummaryRowPosition(int rowPosition) {
        return rowPosition == getSummaryRowPosition();
    }

    /**
     * Get the position of the summary row in this layer.
     *
     * @return The position of the summary row. Typically
     *         <code>rowCount - 1</code>.
     * @since 1.6
     */
    public int getSummaryRowPosition() {
        return getRowCount() - 1;
    }

    @Override
    public boolean doCommand(ILayerCommand command) {
        if (command instanceof RowResizeCommand && command.convertToTargetLayer(this)) {
            RowResizeCommand rowResizeCommand = (RowResizeCommand) command;
            if (isSummaryRowPosition(rowResizeCommand.getRowPosition())) {
                if (rowResizeCommand.downScaleValue() && this.dpiConverter != null) {
                    this.summaryRowHeight = this.dpiConverter.convertDpiToPixel(rowResizeCommand.getNewHeight());
                } else {
                    this.summaryRowHeight = rowResizeCommand.getNewHeight();
                }
                fireLayerEvent(new RowResizeEvent(this, getSummaryRowPosition()));
                return true;
            }
        } else if (command instanceof MultiRowResizeCommand && command.convertToTargetLayer(this)) {
            MultiRowResizeCommand rowResizeCommand = (MultiRowResizeCommand) command;
            for (int row : rowResizeCommand.getRowPositionsArray()) {
                if (isSummaryRowPosition(row)) {
                    if (rowResizeCommand.downScaleValue() && this.dpiConverter != null) {
                        this.summaryRowHeight = this.dpiConverter.convertDpiToPixel(rowResizeCommand.getRowHeight(row));
                    } else {
                        this.summaryRowHeight = rowResizeCommand.getRowHeight(row);
                    }
                    fireLayerEvent(new RowResizeEvent(this, getSummaryRowPosition()));
                    // do not consume as additional rows might need to get
                    // updated too
                }
            }
        } else if (command instanceof CalculateSummaryRowValuesCommand) {
            for (int i = 0; i < getColumnCount(); i++) {
                calculateNewSummaryValue(i, false);
            }
            // we do not return true here, as there might be other layers
            // involved in the composition that also need to calculate
            // the summary values immediately
        } else if (command instanceof DisposeResourcesCommand) {
            this.valueCache.dispose();
        } else if (command instanceof ConfigureScalingCommand) {
            this.dpiConverter = ((ConfigureScalingCommand) command).getVerticalDpiConverter();
        }
        return super.doCommand(command);
    }

    @Override
    public void handleLayerEvent(ILayerEvent event) {
        if (event instanceof IVisualChangeEvent) {
            clearCache();
        }
        super.handleLayerEvent(event);
    }

    /**
     * Clear the internal cache to trigger new calculations.
     * <p>
     * Usually it is not necessary to call this method manually. But for certain
     * use cases it might be useful, e.g. changing the summary provider
     * implementation at runtime.
     *
     * @see CalculatedValueCache#clearCache()
     */
    public void clearCache() {
        this.valueCache.clearCache();
    }

    /**
     * Clears all values in the internal cache to trigger new calculations. This
     * will also clear all values in the cache copy and will result in rendering
     * like there was never a summary value calculated before.
     * <p>
     * Usually it is not necessary to call this method manually. But for certain
     * use cases it might be useful, e.g. changing the summary provider
     * implementation at runtime.
     *
     * @see CalculatedValueCache#killCache()
     */
    public void killCache() {
        this.valueCache.killCache();
    }

    /**
     * This method is a wrapper for calling
     * {@link #getConfigLabelsByPosition(int, int)}. It is needed because
     * sub-classes of the {@link SummaryRowLayer} might perform column
     * position-index transformations, which would be executed again if the call
     * for config labels is nested, e.g. in
     * {@link #calculateNewSummaryValue(int, boolean)}. By providing and using
     * this implementation, sub-classes that perform position-index
     * transformations are able to implement this method by directly accessing
     * the super implementation.
     *
     * @param columnPosition
     *            The column position of the cell for which the config labels
     *            are requested. If transformations are necessary, this value
     *            should be already transformed.
     * @param rowPosition
     *            The row position of the cell for which the config labels are
     *            requested. If transformations are necessary, this value should
     *            be already transformed.
     * @return The {@link LabelStack} for the cell at the given coordinates.
     */
    protected LabelStack getConfigLabelsByPositionWithoutTransformation(int columnPosition, int rowPosition) {
        return this.getConfigLabelsByPosition(columnPosition, rowPosition);
    }

    @Override
    public LabelStack getConfigLabelsByPosition(int columnPosition, int rowPosition) {
        if (isSummaryRowPosition(rowPosition)) {
            // create a new LabelStack that takes the config labels into account
            LabelStack labelStack = new LabelStack();
            if (getConfigLabelAccumulator() != null) {
                getConfigLabelAccumulator().accumulateConfigLabels(labelStack, columnPosition, rowPosition);
            }
            labelStack.addLabelOnTop(DEFAULT_SUMMARY_ROW_CONFIG_LABEL);
            labelStack.addLabelOnTop(DEFAULT_SUMMARY_COLUMN_CONFIG_LABEL_PREFIX + columnPosition);
            return labelStack;
        }
        return super.getConfigLabelsByPosition(columnPosition, rowPosition);
    }

    @Override
    public ILayerCell getCellByPosition(int columnPosition, int rowPosition) {
        if (isSummaryRowPosition(rowPosition)) {
            return new LayerCell(this, columnPosition, rowPosition);
        }
        return super.getCellByPosition(columnPosition, rowPosition);
    }

    @Override
    public int getHeight() {
        if (this.standalone) {
            return getRowHeightByPosition(getSummaryRowPosition());
        }
        return super.getHeight() + getRowHeightByPosition(getSummaryRowPosition());
    }

    @Override
    public int getPreferredHeight() {
        if (this.standalone) {
            return getRowHeightByPosition(getSummaryRowPosition());
        }
        return super.getPreferredHeight() + getRowHeightByPosition(getSummaryRowPosition());
    }

    @Override
    public int getRowCount() {
        if (this.standalone) {
            return 1;
        }
        return super.getRowCount() + 1;
    }

    @Override
    public int getPreferredRowCount() {
        return getRowCount();
    }

    @Override
    public int getRowIndexByPosition(int rowPosition) {
        if (isSummaryRowPosition(rowPosition)) {
            return rowPosition;
        }
        return super.getRowIndexByPosition(rowPosition);
    }

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

    @Override
    public int getRowHeightByPosition(int rowPosition) {
        if (isSummaryRowPosition(rowPosition)) {
            if (this.dpiConverter != null) {
                return this.dpiConverter.convertPixelToDpi(this.summaryRowHeight);
            }
            return this.summaryRowHeight;
        }
        return super.getRowHeightByPosition(rowPosition);
    }

    @Override
    public int getRowPositionByIndex(int rowIndex) {
        if (rowIndex >= 0 && rowIndex < getRowCount()) {
            return rowIndex;
        } else {
            return -1;
        }
    }

    @Override
    public int getColumnPositionByIndex(int columnIndex) {
        if (columnIndex >= 0 && columnIndex < getColumnCount()) {
            return columnIndex;
        } else {
            return -1;
        }
    }

    /**
     *
     * @return <code>true</code> if the summary row is rendered standalone,
     *         <code>false</code> if it is rendered below the underlying layer.
     */
    public boolean isStandalone() {
        return this.standalone;
    }

    /**
     * Configure whether the summary row should be rendered below the underlying
     * layer (<code>false</code>) or if it should be rendered standalone in a
     * separate region of a composite (<code>true</code>).
     *
     * @param standalone
     *            Whether the summary row should be rendered standalone or below
     *            the underlying layer.
     */
    public void setStandalone(boolean standalone) {
        this.standalone = standalone;
    }

    /**
     * @return The {@link ICalculatedValueCache} that contains the summary
     *         values and performs summary calculation in background processes
     *         if necessary.
     * @since 1.3
     */
    public ICalculatedValueCache getValueCache() {
        return this.valueCache;
    }

    /**
     * Set the {@link ICalculatedValueCache} that should be used internally to
     * calculate the summary values in a background thread and cache the
     * results.
     * <p>
     * <b><u>Note:</u></b> By default the {@link CalculatedValueCache} is used.
     * Be sure you know what you are doing when you are trying to exchange the
     * implementation.
     * </p>
     *
     * @param valueCache
     *            The {@link ICalculatedValueCache} that contains the summary
     *            values and performs summary calculation in background
     *            processes if necessary.
     * @since 1.3
     */
    public void setValueCache(ICalculatedValueCache valueCache) {
        this.valueCache = valueCache;
    }

    @Override
    public Collection<ILayer> getUnderlyingLayersByColumnPosition(int columnPosition) {
        // Bug 475766
        // if this SummaryRowLayer is configured to be standalone, return null
        // to ensure that possible transformations are not skipped due to layer
        // composition
        return this.standalone ? null : super.getUnderlyingLayersByColumnPosition(columnPosition);
    }

    @Override
    public Collection<ILayer> getUnderlyingLayersByRowPosition(int rowPosition) {
        // Bug 475766
        // if this SummaryRowLayer is configured to be standalone, return null
        // to ensure that possible transformations are not skipped due to layer
        // composition
        return this.standalone ? null : super.getUnderlyingLayersByRowPosition(rowPosition);
    }

    @Override
    public ILayer getUnderlyingLayerByPosition(int columnPosition, int rowPosition) {
        // Bug 475766
        // if this SummaryRowLayer is configured to be standalone, return null
        // to ensure that possible transformations are not skipped due to layer
        // composition
        return this.standalone ? null : super.getUnderlyingLayerByPosition(columnPosition, rowPosition);
    }

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

        labels.add(DEFAULT_SUMMARY_ROW_CONFIG_LABEL);
        for (int i = 0; i < getColumnCount(); i++) {
            labels.add(DEFAULT_SUMMARY_COLUMN_CONFIG_LABEL_PREFIX + i);
        }

        return labels;
    }
}
