/*******************************************************************************
 * Copyright (c) 2016, 2020 Dirk Fauth.
 *
 * 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:
 *     Dirk Fauth <dirk.fauth@googlemail.com> - initial API and implementation
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.extension.e4.selection;

import java.util.List;

import javax.inject.Inject;

import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
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.RowSelectionModel;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer;
import org.eclipse.nebula.widgets.nattable.selection.SelectionUtils;
import org.eclipse.nebula.widgets.nattable.selection.event.ColumnSelectionEvent;
import org.eclipse.nebula.widgets.nattable.selection.event.ISelectionEvent;

/**
 * Implementation of {@link ILayerListener} to support E4 selection handling.
 * Needs to be set to the {@link SelectionLayer}
 *
 * <pre>
 * E4SelectionListener&lt;Person&gt; esl = new E4SelectionListener&lt;&gt;(service, selectionLayer, bodyDataProvider);
 * selectionLayer.addLayerListener(esl);
 * </pre>
 *
 * @param <T>
 *            The type of objects provided by the {@link IRowDataProvider}
 */
public class E4SelectionListener<T> implements ILayerListener {
    /**
     * The {@link ESelectionService} to set the selection to.
     */
    private ESelectionService selectionService;
    /**
     * The {@link SelectionLayer} that is used to retrieve the selection.
     */
    private SelectionLayer selectionLayer;
    /**
     * The {@link 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 boolean fullySelectedRowsOnly = true;
    /**
     * Flag to configure whether a selection should be set to the
     * {@link ESelectionService} if the row selection changes or if just another
     * column is selected.
     */
    private boolean handleSameRowSelection = false;
    /**
     * Locally stored previous selection which is used to determine if a event
     * for changed selection 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 List<T> previousSelection;

    /**
     * Flag to configure if row selection should be provided in case a column
     * selection happened.
     * <p>
     * To understand this flag consider a {@link SelectionLayer} that is
     * configured for full row selection by using the {@link RowSelectionModel}.
     * On performing column selection, all rows would get selected. This matches
     * the specification because of the {@link RowSelectionModel} and this
     * listener 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;

    /**
     * Flag to configure whether a list of data values should be set as
     * selection or a single value. Needed for creating typed selection
     * listeners.
     */
    private boolean multiSelection = true;

    /**
     * Create a {@link E4SelectionListener} and registers it to the given
     * {@link SelectionLayer}.
     *
     * @param service
     *            The {@link ESelectionService} to which the selection should be
     *            published to.
     * @param selectionLayer
     *            The {@link SelectionLayer} that is used to retrieve the
     *            selection.
     * @param rowDataProvider
     *            The {@link IRowDataProvider} to access the selected row data.
     */
    @Inject
    public E4SelectionListener(
            ESelectionService service,
            SelectionLayer selectionLayer,
            IRowDataProvider<T> rowDataProvider) {

        this.selectionService = service;
        this.selectionLayer = selectionLayer;
        this.rowDataProvider = rowDataProvider;
    }

    @Override
    public void handleLayerEvent(ILayerEvent event) {
        if (event instanceof ISelectionEvent) {
            List<T> selection = SelectionUtils.getSelectedRowObjects(this.selectionLayer, this.rowDataProvider, this.fullySelectedRowsOnly);
            if ((this.handleSameRowSelection || !selection.equals(this.previousSelection))
                    && !(!this.processColumnSelection && event instanceof ColumnSelectionEvent)) {
                try {
                    if (this.multiSelection) {
                        this.selectionService.setSelection(selection);
                    } else {
                        this.selectionService.setSelection(!selection.isEmpty() ? selection.get(0) : null);
                    }
                } finally {
                    this.previousSelection = selection;
                }
            }
        }
    }

    /**
     *
     * @return <code>true</code> if only fully selected rows should be used to
     *         populate the selection or if any selection should be populated.
     *         Default is <code>true</code>.
     */
    public boolean isFullySelectedRowsOnly() {
        return this.fullySelectedRowsOnly;
    }

    /**
     *
     * @param fullySelectedRowsOnly
     *            <code>true</code> if only fully selected rows should be used
     *            to populate the selection <code>false</code> if any selection
     *            should be populated.
     */
    public void setFullySelectedRowsOnly(boolean fullySelectedRowsOnly) {
        this.fullySelectedRowsOnly = fullySelectedRowsOnly;
    }

    /**
     *
     * @return <code>true</code> if a selection should only be handled by the
     *         {@link ESelectionService} if the row selection changes or if just
     *         another column is selected.
     */
    public boolean isHandleSameRowSelection() {
        return this.handleSameRowSelection;
    }

    /**
     *
     * @param handleSameRowSelection
     *            <code>true</code> if a selection should only be handled by the
     *            {@link ESelectionService} if the row selection changes or if
     *            just another column is selected.
     */
    public void setHandleSameRowSelection(boolean handleSameRowSelection) {
        this.handleSameRowSelection = handleSameRowSelection;
    }

    /**
     *
     * @return <code>true</code> if column selection should trigger setting the
     *         selection via {@link ESelectionService} (that means basically a
     *         <i>select all</i>), <code>false</code> if not (needed for huge
     *         datasets to avoid a <i>select all</i>).
     */
    public boolean isProcessColumnSelection() {
        return this.processColumnSelection;
    }

    /**
     * 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 E4SelectionListener#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;
    }

    /**
     *
     * @return <code>true</code> if a list of data values should be set as
     *         selection or a single value.
     */
    public boolean isMultiSelection() {
        return this.multiSelection;
    }

    /**
     * Set this value to <code>true</code> if a list of values should be set as
     * selection to the {@link ESelectionService} or <code>false</code> if only
     * a single value object should be set.
     *
     * @param multiSelection
     *            <code>true</code> if a list of data values should be set as
     *            selection or a single value.
     */
    public void setMultiSelection(boolean multiSelection) {
        this.multiSelection = multiSelection;
    }
}
