/*******************************************************************************
 * Copyright (c) 2009, 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:
 *   Francois Chouinard - Initial API and implementation
 *   Francois Chouinard - Added support for pre-emption
 *   Simon Delisle - Added scheduler for requests
 *******************************************************************************/

package org.eclipse.tracecompass.internal.tmf.core.request;

import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;

import java.util.Queue;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.tracecompass.common.core.log.TraceCompassLog;
import org.eclipse.tracecompass.common.core.log.TraceCompassLogUtils;
import org.eclipse.tracecompass.common.core.log.TraceCompassLogUtils.FlowScopeLog;
import org.eclipse.tracecompass.common.core.log.TraceCompassLogUtils.FlowScopeLogBuilder;
import org.eclipse.tracecompass.internal.tmf.core.TmfCoreTracer;
import org.eclipse.tracecompass.internal.tmf.core.component.TmfEventThread;
import org.eclipse.tracecompass.tmf.core.request.ITmfEventRequest.ExecutionType;

/**
 * The request scheduler works with 5 slots with a specific time. It has 4 slots
 * for foreground requests and 1 slot for background requests, and it passes
 * through all the slots (foreground first and background after).
 *
 * Example: if we have one foreground and one background request, the foreground
 * request will be executed four times more often than the background request.
 *
 * @author Francois Chouinard
 * @author Simon Delisle
 * @version 1.1
 */
public class TmfRequestExecutor implements Executor {

    private static final @NonNull Logger LOGGER = TraceCompassLog.getLogger(TmfRequestExecutor.class);
    private static final @NonNull String LOG_CATEGORY = "RequestExecutor"; //$NON-NLS-1$

    // ------------------------------------------------------------------------
    // Constants
    // ------------------------------------------------------------------------

    private static final long REQUEST_TIME = 100;
    private static final int FOREGROUND_SLOT = 4;

    // ------------------------------------------------------------------------
    // Attributes
    // ------------------------------------------------------------------------

    // The request executor
    private final ExecutorService fExecutor = Executors.newCachedThreadPool();
    private final String fExecutorName;

    // The request queues
    private final Queue<TmfEventThread> fForegroundTasks = new ArrayBlockingQueue<>(10);
    private final Queue<TmfEventThread> fBackgroundTasks = new ArrayBlockingQueue<>(10);

    // The tasks
    private TmfEventThread fActiveTask;

    private Timer fTimer;
    private TimerTask fTimerTask;

    private int fForegroundCycle = 0;

    // ------------------------------------------------------------------------
    // Constructors
    // ------------------------------------------------------------------------

    /**
     * Default constructor
     */
    public TmfRequestExecutor() {
        // We know the canonical name is not null because we use ExecutorService
        // only
        String canonicalName = checkNotNull(fExecutor.getClass().getCanonicalName());
        fExecutorName = canonicalName.substring(canonicalName.lastIndexOf('.') + 1);
        if (TmfCoreTracer.isComponentTraced()) {
            TmfCoreTracer.trace(fExecutor + " created"); //$NON-NLS-1$
        }
    }

    // ------------------------------------------------------------------------
    // Getters
    // ------------------------------------------------------------------------

    /**
     * @return the shutdown state (i.e. if it is accepting new requests)
     */
    public boolean isShutdown() {
        return fExecutor.isShutdown();
    }

    /**
     * @return the termination state
     */
    public boolean isTerminated() {
        return fExecutor.isTerminated();
    }

    // ------------------------------------------------------------------------
    // Operations
    // ------------------------------------------------------------------------

    /**
     * Initialize the executor
     */
    public void init() {
        if (fTimer != null) {
            return;
        }
        // Initialize the timer for the schedSwitch
        fTimerTask = new SchedSwitch();
        fTimer = new Timer(true);
        fTimer.schedule(fTimerTask, 0, REQUEST_TIME);
    }

    @Override
    public synchronized void execute(final Runnable command) {

        // We are expecting MyEventThread:s
        if (!(command instanceof TmfEventThread)) {
            TraceCompassLogUtils.traceInstant(LOGGER, Level.WARNING, "RequestExecutor:NotATmfEventThread", "cmd", command.toString()); //$NON-NLS-1$ //$NON-NLS-2$
            return;
        }

        try (FlowScopeLog scope = new FlowScopeLogBuilder(LOGGER, Level.FINE, "RequestExecutor:CreatingThread").setCategory(LOG_CATEGORY).build()) { //$NON-NLS-1$
            // Wrap the thread in a MyThread
            TmfEventThread thread = (TmfEventThread) command;
            TmfEventThread wrapper = new TmfEventThread(thread) {
                @Override
                public void run() {
                    try (FlowScopeLog log = new FlowScopeLogBuilder(LOGGER, Level.FINE, "RequestExecutor:RunningRequest", "thread", thread.getThread(), "execution type", thread.getExecType()).setParentScope(scope).build()) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                        thread.run();
                    } finally {
                        scheduleNext();
                    }
                }
            };

            // Add the thread to the appropriate queue
            ExecutionType priority = thread.getExecType();

            if (priority == ExecutionType.FOREGROUND) {
                if (!fForegroundTasks.offer(wrapper)) {
                    wrapper.cancel();
                }
            } else {
                if (!fBackgroundTasks.offer(wrapper)) {
                    wrapper.cancel();
                }
            }
        }
    }

    /**
     * Timer task to trigger scheduleNext()
     */
    private class SchedSwitch extends TimerTask {

        SchedSwitch() {
        }

        @Override
        public void run() {
            scheduleNext();
        }
    }

    /**
     * Executes the next pending request, if applicable.
     */
    protected synchronized void scheduleNext() {
        if (!isShutdown()) {
            if (fActiveTask == null) {
                schedule();
            } else if (fActiveTask.getExecType() == ExecutionType.FOREGROUND) {
                if (fActiveTask.getThread().isCompleted()) {
                    fActiveTask = null;
                    schedule();
                } else {
                    if (hasTasks()) {
                        fActiveTask.getThread().suspend();
                        if (!fForegroundTasks.offer(fActiveTask)) {
                            fActiveTask.cancel();
                            fActiveTask = null;
                        }
                        schedule();
                    }
                }

            } else if (fActiveTask.getExecType() == ExecutionType.BACKGROUND) {
                if (fActiveTask.getThread().isCompleted()) {
                    fActiveTask = null;
                    schedule();
                } else {
                    if (hasTasks()) {
                        fActiveTask.getThread().suspend();
                        if (!fBackgroundTasks.offer(fActiveTask)) {
                            fActiveTask.cancel();
                            fActiveTask = null;
                        }
                        schedule();
                    }
                }
            }
        }
    }

    /**
     * Stops the executor
     */
    public synchronized void stop() {
        if (fTimerTask != null) {
            fTimerTask.cancel();
        }

        if (fTimer != null) {
            fTimer.cancel();
        }

        if (fActiveTask != null) {
            fActiveTask.cancel();
        }

        while ((fActiveTask = fForegroundTasks.poll()) != null) {
            fActiveTask.cancel();
        }
        while ((fActiveTask = fBackgroundTasks.poll()) != null) {
            fActiveTask.cancel();
        }

        fExecutor.shutdown();
        if (TmfCoreTracer.isComponentTraced()) {
            TmfCoreTracer.trace(fExecutor + " terminated"); //$NON-NLS-1$
        }
    }

    // ------------------------------------------------------------------------
    // Helper methods
    // ------------------------------------------------------------------------

    /**
     * Determine which type of request (foreground or background) we schedule
     * next
     */
    private void schedule() {
        if (!fForegroundTasks.isEmpty()) {
            scheduleNextForeground();
        } else {
            scheduleNextBackground();
        }
    }

    /**
     * Schedule the next foreground request
     */
    private void scheduleNextForeground() {
        if (fForegroundCycle < FOREGROUND_SLOT || fBackgroundTasks.isEmpty()) {
            ++fForegroundCycle;
            fActiveTask = fForegroundTasks.poll();
            executefActiveTask();
        } else {
            fActiveTask = null;
            scheduleNextBackground();
        }
    }

    /**
     * Schedule the next background request
     */
    private void scheduleNextBackground() {
        fForegroundCycle = 0;
        if (!fBackgroundTasks.isEmpty()) {
            fActiveTask = fBackgroundTasks.poll();
            executefActiveTask();
        }
    }

    /**
     * Execute or resume the active task
     */
    private void executefActiveTask() {
        if (fActiveTask.getThread().isPaused()) {
            fActiveTask.getThread().resume();
        } else {
            fExecutor.execute(fActiveTask);
        }
    }

    /**
     * Check if the scheduler has tasks
     */
    private boolean hasTasks() {
        return !(fForegroundTasks.isEmpty() && fBackgroundTasks.isEmpty());
    }

    // ------------------------------------------------------------------------
    // Object
    // ------------------------------------------------------------------------

    @Override
    public String toString() {
        return "[TmfRequestExecutor(" + fExecutorName + ")]"; //$NON-NLS-1$ //$NON-NLS-2$
    }

}
