/*_############################################################################
  _## 
  _##  SNMP4J - TaskScheduler.java  
  _## 
  _##  Copyright (C) 2003-2026  Frank Fock (SNMP4J.org)
  _##  
  _##  Licensed under the Apache License, Version 2.0 (the "License");
  _##  you may not use this file except in compliance with the License.
  _##  You may obtain a copy of the License at
  _##  
  _##      http://www.apache.org/licenses/LICENSE-2.0
  _##  
  _##  Unless required by applicable law or agreed to in writing, software
  _##  distributed under the License is distributed on an "AS IS" BASIS,
  _##  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  _##  See the License for the specific language governing permissions and
  _##  limitations under the License.
  _##  
  _##########################################################################*/


package org.snmp4j.util;

import java.util.LinkedList;
import org.snmp4j.log.LogFactory;
import org.snmp4j.log.LogAdapter;

/**
 * The {@code TaskScheduler} uses a {@code ThreadPool} to recurrent
 * execute {@code SchedulerTask}s.
 *
 * @author Frank Fock
 * @version 1.6
 * @since 1.6
 */
public class TaskScheduler implements Runnable {

    private final LogAdapter logger = LogFactory.getLogger(TaskScheduler.class);

    private static final long DEFAULT_SCHEDULER_TIMEOUT = 5;

    private final LinkedList<SchedulerTask> tasks = new LinkedList<SchedulerTask>();
    private final ThreadPool threadPool;
    private boolean stop;
    protected long schedulerTimeout = DEFAULT_SCHEDULER_TIMEOUT;

    /**
     * Creates a {@code TaskScheduler} that uses the supplied
     * {@code ThreadPool} to execute tasks.
     *
     * @param threadPool
     *    a {@code ThreadPool}.
     */
    public TaskScheduler(ThreadPool threadPool) {
        this.threadPool = threadPool;
    }

    /**
     * Adds a task to the scheduler.
     * @param task
     *    a {@code SchedulerTask}.
     */
    public synchronized void addTask(SchedulerTask task) {
        tasks.addLast(task);
        notifyAll();
    }

    /**
     * Removes a task from the scheduler.
     * @param task
     *    the {@code SchedulerTask} to be removed from the scheduler
     * @return
     *    {@code true} if the task could be removed.
     */
    public synchronized boolean removeTask(SchedulerTask task) {
        return tasks.remove(task);
    }

    /**
     * Removes all tasks.
     */
    public synchronized void clear() {
        tasks.clear();
    }

    /**
     * Runs the scheduler. While in this method tasks are scheduled on the
     * internal thread pool. The scheduler tries to schedule tasks fairly.
     * <p>
     * The {@link TaskScheduler} monitor is held only while the task list is being
     * inspected/mutated. Submission to the {@link ThreadPool} (including the
     * {@link ThreadPool#wait(long) wait} for capacity when the pool is busy) happens
     * <em>without</em> the {@code TaskScheduler.this} monitor, so concurrent
     * {@link #addTask}, {@link #removeTask}, and {@link #clear} calls — including recursive
     * {@code addTask} invocations from worker threads running in the pool itself — cannot
     * deadlock against a saturated pool.
     */
    public void run() {
        while (!stop) {
            SchedulerTask taskToRun;
            // Hold the scheduler monitor only long enough to sweep finished tasks and pick the
            // next ready one (rotated to the tail of the queue for fairness).
            synchronized (this) {
                taskToRun = pollNextReadyTask();
            }
            if (taskToRun != null) {
                submitToPool(taskToRun);
            }
            else {
                waitForWork();
            }
        }
        logger.info("Scheduler stopped.");
    }

    /**
     * Removes all {@link SchedulerTask#isDone() done} tasks from the queue and returns the next
     * {@link SchedulerTask#isReadyToRun() ready} task, rotated to the tail of the queue so other
     * ready tasks get their turn first on the following pass. Caller must hold
     * {@code TaskScheduler.this}.
     *
     * @return the next ready task, or {@code null} if no task is currently ready.
     */
    private SchedulerTask pollNextReadyTask() {
        SchedulerTask result = null;
        for (int i = 0; i < tasks.size(); ) {
            SchedulerTask task = tasks.get(i);
            if (task.isDone()) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Task '" + task + "' is done");
                }
                tasks.remove(i);
                continue;
            }
            if (result == null && task.isReadyToRun()) {
                result = task;
                tasks.remove(i);
                tasks.addLast(task);
                // Skip increment — the rotation kept the iteration position aligned with
                // the next, not-yet-examined task in the (now-shifted) queue.
                continue;
            }
            i++;
        }
        return result;
    }

    /**
     * Submits {@code task} to the thread pool, waiting (without holding
     * {@code TaskScheduler.this}) for capacity to become available when the pool is busy.
     * Returns when the task has been accepted by the pool or the scheduler has been stopped /
     * interrupted.
     *
     * @param task the task to execute on the pool.
     */
    private void submitToPool(SchedulerTask task) {
        while (!stop && !threadPool.tryToExecute(task)) {
            try {
                synchronized (threadPool) {
                    // Re-check inside the monitor to avoid missing a notification published
                    // between the failed tryToExecute above and acquiring the pool's monitor.
                    if (!threadPool.tryToExecute(task)) {
                        threadPool.wait(schedulerTimeout);
                    }
                }
            }
            catch (InterruptedException ex) {
                logger.warn("Scheduler interrupted, aborting...");
                stop = true;
            }
        }
    }

    /**
     * Park the scheduler thread briefly when there is nothing to do. Waits on the scheduler
     * itself when the pool is idle (so a fresh {@link #addTask} can wake us via
     * {@link Object#notifyAll()}); otherwise waits on the pool so worker completions can wake us.
     * Never holds two monitors at once.
     */
    private void waitForWork() {
        try {
            if (threadPool.isIdle()) {
                synchronized (this) {
                    wait(schedulerTimeout);
                }
            }
            else {
                synchronized (threadPool) {
                    threadPool.wait(schedulerTimeout);
                }
            }
        }
        catch (InterruptedException ex) {
            logger.warn("Scheduler interrupted, aborting...");
            stop = true;
        }
    }

    /**
     * Stops the schedulers run method.
     * @param stop
     *    {@code true} to stop the scheduler.
     */
    public void setStop(boolean stop) {
        this.stop = stop;
    }

    /**
     * Checks if the scheduler is (to be) stopped.
     * @return
     *    {@code true} if the scheduler has been stopped or is being stopped.
     */
    public boolean isStop() {
        return stop;
    }
}
