/*******************************************************************************
 * 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.selection;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.nebula.widgets.nattable.data.IRowDataProvider;
import org.eclipse.nebula.widgets.nattable.layer.ILayerListener;
import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent;
import org.eclipse.nebula.widgets.nattable.selection.command.SelectRowsCommand;
import org.eclipse.nebula.widgets.nattable.selection.event.ColumnSelectionEvent;
import org.eclipse.nebula.widgets.nattable.selection.event.ISelectionEvent;
import org.eclipse.nebula.widgets.nattable.util.ObjectUtils;

/**
 * Implementation of ISelectionProvider to add support for JFace selection
 * handling.
 *
 * @param <T>
 *            The type of objects provided by the IRowDataProvider
 */
public class RowSelectionProvider<T> implements ISelectionProvider, ILayerListener {

    /**
     * The SelectionLayer this ISelectionProvider is connected to.
     */
    private SelectionLayer selectionLayer;
    /**
     * The IRowDataProvider to access the selected row data.
     */
    private IRowDataProvider<T> rowDataProvider;
    /**
     * Flag to determine if only fully selected rows should be used to populate
     * the selection or if any selection should be populated. Default is to only
     * populate fully selected rows.
     */
    private final boolean fullySelectedRowsOnly;
    /**
     * Flag to configure whether only SelectionChangedEvents should be fired if
     * the row selection changes or even if you just select another column.
     */
    private final boolean handleSameRowSelection;
    /**
     * Collection of ISelectionChangedListeners to this ISelectionProvider
     */
    private Set<ISelectionChangedListener> listeners = new HashSet<>();
    /**
     * Locally stored previous selection which is used to determine if a
     * SelectionChangedEvent should be fired. It is used to avoid firing events
     * if the same row is selected again (default). If handleSameRowSelection is
     * set to <code>true</code>, this value is not evaluated at runtime.
     */
    private ISelection previousSelection;

    /**
     * Flag to configure whether <code>setSelection()</code> should add or set
     * the selection.
     * <p>
     * This was added for convenience because the initial code always added the
     * selection on <code>setSelection()</code> by creating a SelectRowsCommand
     * with the withControlMask set to <code>true</code>. Looking at the
     * specification, <code>setSelection()</code> is used to set the <b>new</b>
     * selection. So the default here is now to set instead of add. But for
     * convenience to older code that relied on the add behaviour it is now
     * possible to change it back to adding.
     * </p>
     */
    private boolean addSelectionOnSet = false;

    /**
     * Flag to configure if row selection should be provided in case a column
     * selection happened.
     * <p>
     * To understand this flag consider a SelectionLayer that is configured for
     * full row selection by using the RowSelectionModel. On performing column
     * selection, all rows would get selected. This matches the specification
     * because of the RowSelectionModel and this RowSelectionProvider would
     * process every selected row.
     * </p>
     * <p>
     * As described in Bug 421848 this causes serious issues for huge datasets.
     * To avoid such issues by still providing the ability to perform and
     * provide multiple row selections, this flag was introduced. Setting the
     * value to <code>false</code> will avoid processing in case of column
     * selections.
     * </p>
     *
     * @see RowSelectionModel
     */
    private boolean processColumnSelection = true;

    /**
     * Create a RowSelectionProvider that only handles fully selected rows and
     * only fires SelectionChangedEvents if the row selection changes.
     *
     * @param selectionLayer
     *            The SelectionLayer this ISelectionProvider should be connected
     *            to.
     * @param rowDataProvider
     *            The IRowDataProvider that should be used to access the
     *            selected row data.
     */
    public RowSelectionProvider(
            SelectionLayer selectionLayer,
            IRowDataProvider<T> rowDataProvider) {

        this(selectionLayer, rowDataProvider, true);
    }

    /**
     * Create a RowSelectionProvider that only fires SelectionChangedEvents if
     * the row selection changes.
     *
     * @param selectionLayer
     *            The SelectionLayer this ISelectionProvider should be connected
     *            to.
     * @param rowDataProvider
     *            The IRowDataProvider that should be used to access the
     *            selected row data.
     * @param fullySelectedRowsOnly
     *            Flag to determine if only fully selected rows should be used
     *            to populate the selection or if any selection should be
     *            populated.
     */
    public RowSelectionProvider(
            SelectionLayer selectionLayer,
            IRowDataProvider<T> rowDataProvider,
            boolean fullySelectedRowsOnly) {

        this(selectionLayer, rowDataProvider, fullySelectedRowsOnly, false);
    }

    /**
     * Create a RowSelectionProvider configured with the given parameters.
     *
     * @param selectionLayer
     *            The SelectionLayer this ISelectionProvider should be connected
     *            to.
     * @param rowDataProvider
     *            The IRowDataProvider that should be used to access the
     *            selected row data.
     * @param fullySelectedRowsOnly
     *            Flag to determine if only fully selected rows should be used
     *            to populate the selection or if any selection should be
     *            populated.
     * @param handleSameRowSelection
     *            Flag to configure whether only SelectionChangedEvents should
     *            be fired if the row selection changes or even if you just
     *            select another column.
     */
    public RowSelectionProvider(
            SelectionLayer selectionLayer,
            IRowDataProvider<T> rowDataProvider,
            boolean fullySelectedRowsOnly,
            boolean handleSameRowSelection) {

        this.selectionLayer = selectionLayer;
        this.rowDataProvider = rowDataProvider;
        this.fullySelectedRowsOnly = fullySelectedRowsOnly;
        this.handleSameRowSelection = handleSameRowSelection;

        this.selectionLayer.addLayerListener(this);
    }

    /**
     * Updates this RowSelectionProvider so it handles the selection of another
     * SelectionLayer and IRowDataProvider.
     * <p>
     * This method was introduced to add support for multiple selection provider
     * within one part. As replacing the selection provider during the lifetime
     * of a part is not properly supported by the workbench, this implementation
     * adds the possibility to exchange the control that serves as selection
     * provider by exchanging the references in the selection provider itself.
     * </p>
     *
     * @param selectionLayer
     *            The SelectionLayer this ISelectionProvider should be connected
     *            to.
     * @param rowDataProvider
     *            The IRowDataProvider that should be used to access the
     *            selected row data.
     */
    public void updateSelectionProvider(SelectionLayer selectionLayer, IRowDataProvider<T> rowDataProvider) {
        // unregister as listener from the current set SelectionLayer
        this.selectionLayer.removeLayerListener(this);

        // update the references on which this RowSelectionProvider should
        // operate
        this.selectionLayer = selectionLayer;
        this.rowDataProvider = rowDataProvider;

        // register on the new set SelectionLayer as listener
        this.selectionLayer.addLayerListener(this);
    }

    @Override
    public void addSelectionChangedListener(ISelectionChangedListener listener) {
        if (listener != null) {
            this.listeners.add(listener);
        }
    }

    @Override
    public ISelection getSelection() {
        return populateRowSelection(this.selectionLayer, this.rowDataProvider, this.fullySelectedRowsOnly);
    }

    @Override
    public void removeSelectionChangedListener(ISelectionChangedListener listener) {
        this.listeners.remove(listener);
    }

    @SuppressWarnings("unchecked")
    @Override
    public void setSelection(ISelection selection) {
        if (this.selectionLayer != null && selection instanceof IStructuredSelection) {
            if (!this.addSelectionOnSet || selection.isEmpty()) {
                this.selectionLayer.clear(false);
            }
            if (!selection.isEmpty()) {
                List<T> rowObjects = ((IStructuredSelection) selection).toList();
                Set<Integer> rowPositions = new HashSet<>();
                for (T rowObject : rowObjects) {
                    int rowIndex = this.rowDataProvider.indexOfRowObject(rowObject);
                    int rowPosition = this.selectionLayer.getRowPositionByIndex(rowIndex);
                    rowPositions.add(Integer.valueOf(rowPosition));
                }
                int intValue = -1;
                if (!rowPositions.isEmpty()) {
                    Integer max = Collections.max(rowPositions);
                    intValue = max.intValue();
                }
                if (intValue >= 0) {
                    this.selectionLayer.doCommand(
                            new SelectRowsCommand(
                                    this.selectionLayer,
                                    0,
                                    ObjectUtils.asIntArray(rowPositions),
                                    false,
                                    true,
                                    intValue));
                }
            } else {
                this.selectionLayer.fireCellSelectionEvent(
                        this.selectionLayer.getLastSelectedCell().columnPosition,
                        this.selectionLayer.getLastSelectedCell().rowPosition,
                        false, false, false);
            }
        }
    }

    @Override
    public void handleLayerEvent(ILayerEvent event) {
        if (event instanceof ISelectionEvent) {
            ISelection selection = getSelection();
            if ((this.handleSameRowSelection || !selection.equals(this.previousSelection))
                    && !(!this.processColumnSelection && event instanceof ColumnSelectionEvent)) {
                try {
                    for (ISelectionChangedListener listener : this.listeners) {
                        listener.selectionChanged(new SelectionChangedEvent(this, selection));
                    }
                } finally {
                    this.previousSelection = selection;
                }
            }
        }
    }

    static <T> StructuredSelection populateRowSelection(
            SelectionLayer selectionLayer,
            IRowDataProvider<T> rowDataProvider,
            boolean fullySelectedRowsOnly) {
        List<T> rowObjects = SelectionUtils.getSelectedRowObjects(selectionLayer, rowDataProvider, fullySelectedRowsOnly);
        return rowObjects.isEmpty() ? StructuredSelection.EMPTY : new StructuredSelection(rowObjects);
    }

    /**
     * Configure whether <code>setSelection()</code> should add or set the
     * selection.
     * <p>
     * This was added for convenience because the initial code always added the
     * selection on <code>setSelection()</code> by creating a SelectRowsCommand
     * with the withControlMask set to <code>true</code>. Looking at the
     * specification, <code>setSelection()</code> is used to set the <b>new</b>
     * selection. So the default here is now to set instead of add. But for
     * convenience to older code that relied on the add behaviour it is now
     * possible to change it back to adding.
     * </p>
     *
     * @param addSelectionOnSet
     *            <code>true</code> to add the selection on calling
     *            <code>setSelection()</code> The default is <code>false</code>
     *            to behave like specified in RowSelectionProvider
     */
    public void setAddSelectionOnSet(boolean addSelectionOnSet) {
        this.addSelectionOnSet = addSelectionOnSet;
    }

    /**
     * Configure whether column selections should start row selection processing
     * or not.
     * <p>
     * This is necessary to handle issues with huge datasets. Dependent on
     * different configurations, selecting a column can cause the selection of
     * all rows in a table, which would then lead to populate the whole dataset
     * as selection via this provider. Setting the
     * {@link RowSelectionProvider#processColumnSelection} flag to
     * <code>false</code> will skip processing to avoid such issues.
     * </p>
     *
     * @param processColumnSelection
     *            <code>true</code> to process row selection in case of column
     *            selections (default) <code>false</code> to skip processing in
     *            case of column selections to avoid issues on large datasets.
     */
    public void setProcessColumnSelection(boolean processColumnSelection) {
        this.processColumnSelection = processColumnSelection;
    }

}
