/*****************************************************************************
 * Copyright (c) 2018, 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.hierarchical;

import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.eclipse.nebula.widgets.nattable.command.DisposeResourcesCommand;
import org.eclipse.nebula.widgets.nattable.command.ILayerCommandHandler;
import org.eclipse.nebula.widgets.nattable.grid.cell.AlternatingRowConfigLabelAccumulator;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.layer.ILayerListener;
import org.eclipse.nebula.widgets.nattable.layer.LabelStack;
import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.RowStructuralChangeEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.RowStructuralRefreshEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.VisualRefreshEvent;
import org.eclipse.nebula.widgets.nattable.resize.event.RowResizeEvent;
import org.eclipse.swt.widgets.Display;

/**
 * Specialization of the {@link AlternatingRowConfigLabelAccumulator} that
 * calculates the even/odd row labels in a hierarchical tree by inspecting the
 * row spanning of the first level node. For better performance the calculation
 * results are cached. As the cache needs to be cleared on structural changes,
 * this class also implements the {@link ILayerListener} to clear the cache
 * automatically on {@link RowStructuralChangeEvent}s if registered on the given
 * layer via {@link ILayer#addLayerListener(ILayerListener)}.
 *
 * @since 1.6
 */
public class HierarchicalTreeAlternatingRowConfigLabelAccumulator
        extends AlternatingRowConfigLabelAccumulator
        implements ILayerListener, ILayerCommandHandler<DisposeResourcesCommand> {

    private ExecutorService executor =
            Executors.newSingleThreadExecutor(r -> new Thread(r, "HierarchicalTreeAlternatingRowConfigLabelAccumulator")); //$NON-NLS-1$
    private Future<Void> future = null;

    private ConcurrentHashMap<Integer, String> rowLabelCache = new ConcurrentHashMap<>();

    /**
     *
     * @param layer
     *            The {@link ILayer} that is used to determine the row spanning
     *            in the first column. Should be the HierarchicalTreeLayer.
     */
    public HierarchicalTreeAlternatingRowConfigLabelAccumulator(ILayer layer) {
        super(layer);
    }

    @Override
    public void accumulateConfigLabels(LabelStack configLabels, int columnPosition, int rowPosition) {
        ILayerCell cell = this.layer.getCellByPosition(0, rowPosition);
        if (cell != null) {
            String label = this.rowLabelCache.get(cell.getOriginRowPosition());
            if (label == null) {
                if (this.future == null || this.future.isCancelled() || this.future.isDone()) {
                    calculateLabels();
                }

                if (rowPosition < 100) {
                    // for the first few rows we wait a moment to give the
                    // background calculation time to proceed to avoid
                    // flickering at the top of the table. we do not wait in any
                    // case as otherwise sorting would show a bad performance in
                    // the middle or the end of the table
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        // Restore interrupted state...
                        Thread.currentThread().interrupt();
                    }
                    label = this.rowLabelCache.get(cell.getOriginRowPosition());

                    if (label != null) {
                        configLabels.addLabel(label);
                    }
                }
            } else {
                configLabels.addLabel(label);
            }
        }
    }

    /**
     * Triggers a new background thread for calculation of the row label cache.
     */
    public void calculateLabels() {
        this.future = this.executor.submit(() -> {
            String lastKnownLabel = EVEN_ROW_CONFIG_TYPE;
            HierarchicalTreeAlternatingRowConfigLabelAccumulator.this.rowLabelCache.put(0, lastKnownLabel);

            int row = 0;
            while (row < HierarchicalTreeAlternatingRowConfigLabelAccumulator.this.layer.getRowCount()) {

                if (Thread.currentThread().isInterrupted()) {
                    return null;
                }

                // determine the next row after the last known based
                // on spanning
                ILayerCell lastKnownCell = HierarchicalTreeAlternatingRowConfigLabelAccumulator.this.layer.getCellByPosition(0, row);
                if (lastKnownCell != null) {
                    row = lastKnownCell.getOriginRowPosition() + lastKnownCell.getRowSpan();

                    lastKnownLabel = ODD_ROW_CONFIG_TYPE.equals(lastKnownLabel) ? EVEN_ROW_CONFIG_TYPE : ODD_ROW_CONFIG_TYPE;

                    HierarchicalTreeAlternatingRowConfigLabelAccumulator.this.rowLabelCache.put(row, lastKnownLabel);
                } else {
                    // if for some case there is no lastKnownCell we break
                    // otherwise we end up in a endless loop
                    break;
                }
            }

            // once the calculation is done we trigger a repaint to ensure
            // the correct alternate colors are rendered
            Display.getDefault().asyncExec(() -> HierarchicalTreeAlternatingRowConfigLabelAccumulator.this.layer.fireLayerEvent(
                    new VisualRefreshEvent(HierarchicalTreeAlternatingRowConfigLabelAccumulator.this.layer)));

            return null;
        });
    }

    /**
     * Clears the local cache of calculated row position to label mappings.
     */
    public void clearCache() {
        if (this.future != null && !this.future.isCancelled() && !this.future.isDone()) {
            // cancel a already running process
            this.future.cancel(true);
            // ensure to wait until the current running future is terminated
            // before starting a new calculation so there are no concurrent
            // write operations when starting the new calculation
            try {
                this.future.get();
            } catch (InterruptedException e) {
                // Restore interrupted state...
                Thread.currentThread().interrupt();
            } catch (ExecutionException | CancellationException e) {
                // nothing to do here
            }
        }
        this.rowLabelCache.clear();
        // trigger calculation
        calculateLabels();
    }

    @Override
    public void handleLayerEvent(ILayerEvent event) {
        // if there are structural changes to rows that are not related to
        // resizing, we need to clear the cache
        if ((event instanceof RowStructuralChangeEvent && !(event instanceof RowResizeEvent))
                || event instanceof RowStructuralRefreshEvent) {
            clearCache();
        }
    }

    @Override
    public boolean doCommand(ILayer targetLayer, DisposeResourcesCommand command) {
        if (!this.executor.isShutdown()) {
            // simply shutdown the executor, no need to await termination on
            // dispose
            this.executor.shutdownNow();
        }
        // the DisposeResourcesCommand should not be consumed
        return false;
    }

    @Override
    public Class<DisposeResourcesCommand> getCommandClass() {
        return DisposeResourcesCommand.class;
    }
}
