/*******************************************************************************
 * Copyright (c) 2014 Ericsson
 *
 * All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License 2.0 which
 * accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Alexandre Montplaisir - Initial API and implementation
 *     Patrick Tasse - Update queue handling
 *******************************************************************************/

package org.eclipse.tracecompass.tmf.ui;

import java.util.LinkedHashMap;
import java.util.Map;

import org.eclipse.swt.widgets.Display;
import org.eclipse.tracecompass.internal.tmf.ui.Activator;

/**
 * This handler offers "coalescing" of UI updates.
 *
 * When displaying live experiments containing a high number of traces, every
 * trace will want to regularly update views with their new available data. This
 * can cause a high number of threads calling {@link Display#asyncExec}
 * repeatedly, which can really impede UI responsiveness.
 * <p>
 * Instead of calling {@link Display#asyncExec} directly, threads that want to
 * queue updates to the UI can instead call
 * {@link TmfUiRefreshHandler#queueUpdate}. If the handler is not currently
 * executing another update it will be scheduled immediately. Otherwise the
 * update will be queued.
 * <p>
 * The handler will only execute one update at a time. While it is busy, new
 * requests received from a source that is already in the queue will replace the
 * previous one (as we assume the latest UI update request is the most
 * up-to-date and interesting one), preserving the previous request order. New
 * requests received from other sources will be added to the end of the queue
 * (keeping only the latest request from each source).
 * <p>
 * Once the current update is completed, the oldest request in the queue will be
 * sent to the UI thread via one single call to {@link Display#syncExec}.
 *
 * @author Alexandre Montplaisir
 */
public class TmfUiRefreshHandler {

    /** Singleton instance */
    private static TmfUiRefreshHandler fInstance = null;

    private final Map<Object, Runnable> fUpdates = new LinkedHashMap<>();
    private Thread fCurrentTask;


    /**
     * Internal constructor
     */
    private TmfUiRefreshHandler() {
        fCurrentTask = null;
    }

    /**
     * Get the handler's instance
     *
     * @return The singleton instance
     */
    public static synchronized TmfUiRefreshHandler getInstance() {
        if (fInstance == null) {
            fInstance = new TmfUiRefreshHandler();
        }
        return fInstance;
    }

    /**
     * Cancel all current requests and dispose the handler.
     */
    public synchronized void dispose() {
        fUpdates.clear();
        fCurrentTask = null;
    }

    /**
     * Queue a UI update. Threads that want to benefit from "UI coalescing"
     * should send their {@link Runnable} to this method, instead of
     * {@link Display#asyncExec(Runnable)}.
     *
     * @param source
     *            The source sending the request. Typically callers should use
     *            "this". When multiple requests are queued before being
     *            executed, only the latest request per source is actually sent.
     * @param task
     *            The {@link Runnable} to execute in the UI thread.
     */
    public synchronized void queueUpdate(Object source, Runnable task) {
        fUpdates.put(source, task);
        if (fCurrentTask == null) {
            fCurrentTask = new RunAllUpdates();
            fCurrentTask.start();
        }
    }

    /**
     * Task to empty the update queue, and send each task to the UI thread.
     */
    private class RunAllUpdates extends Thread {
        @Override
        public void run() {
            while (true) {
                Runnable nextTask = null;
                synchronized (TmfUiRefreshHandler.this) {
                    if (!fUpdates.isEmpty()) {
                        Object firstKey = fUpdates.keySet().iterator().next();
                        nextTask = fUpdates.get(firstKey);
                        fUpdates.remove(firstKey);
                    }
                    if (nextTask == null) {
                        /*
                         * No updates remaining in the queue, put fCurrentTask
                         * back to null so that a new task can be scheduled.
                         */
                        fCurrentTask = null;
                        break;
                    }
                }
                try {
                    Display.getDefault().syncExec(nextTask);
                } catch (Exception e) {
                    Activator.getDefault().logError("Exception thrown by TmfUiRefreshHandler task", e); //$NON-NLS-1$
                }
            }
        }
    }
}
