package org.infinispan.executors;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

import org.infinispan.commons.IllegalLifecycleStateException;
import org.infinispan.util.concurrent.WithinThreadExecutor;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;

/**
 * Executes tasks in the given executor, but never has more than {@code maxConcurrentTasks} tasks running at the same time.
 *
 * @author Dan Berindei
 * @since 7.2
 */
public class SemaphoreCompletionService<T> implements CompletionService<T> {
   private static final Log log = LogFactory.getLog(SemaphoreCompletionService.class);

   private final Executor executor;
   private final CustomSemaphore semaphore;
   private final BlockingQueue<QueueingTask> queue;
   private final BlockingQueue<QueueingTask> completionQueue = new LinkedBlockingQueue<>();
   private final boolean blocking;

   public SemaphoreCompletionService(Executor executor, int maxConcurrentTasks) {
      this.executor = executor;
      this.semaphore = new CustomSemaphore(maxConcurrentTasks);
      // Users of WithinThreadExecutor expect the tasks to execute in the thread that submitted them
      // But with a LinkedBlockingQueue, they could execute on any thread calling backgroundTaskFinished.
      this.blocking = executor instanceof WithinThreadExecutor;
      this.queue = blocking ? new SynchronousQueue<>() : new LinkedBlockingQueue<>();
   }

   public List<? extends Future<T>> drainCompletionQueue() {
      List<QueueingTask> list = new ArrayList<QueueingTask>();
      completionQueue.drainTo(list);
      return list;
   }

   /**
    * When stopping, cancel any queued tasks.
    */
   public void cancelQueuedTasks() {
      ArrayList<QueueingTask> queuedTasks = new ArrayList<QueueingTask>();
      queue.drainTo(queuedTasks);
      for (QueueingTask task : queuedTasks) {
         task.cancel(false);
      }
   }

   /**
    * Called from a task to remove the permit that would otherwise be freed when the task finishes running
    *
    * When the asynchronous part of the task finishes, it must call {@link #backgroundTaskFinished(Callable)}
    * to make the permit available again.
    */
   public void continueTaskInBackground() {
      if (log.isTraceEnabled()) log.tracef("Moving task to background, available permits %d", semaphore.availablePermits());
      // Prevent other tasks from running with this task's permit
      semaphore.removePermit();
   }

   /**
    * Signal that a task that called {@link #continueTaskInBackground()} has finished and
    * optionally execute another task on the just-freed thread.
    */
   public Future<T> backgroundTaskFinished(final Callable<T> cleanupTask) {
      QueueingTask futureTask = null;
      if (cleanupTask != null) {
         if (log.isTraceEnabled()) log.tracef("Background task finished, executing cleanup task");
         futureTask = new QueueingTask(cleanupTask);
         executor.execute(futureTask);
      } else {
         semaphore.release();
         if (log.isTraceEnabled()) log.tracef("Background task finished, available permits %d", semaphore.availablePermits());
         executeFront();
      }
      return futureTask;
   }

   @Override
   public Future<T> submit(final Callable<T> task) {
      QueueingTask futureTask = new QueueingTask(task);
      return doSubmit(futureTask);
   }

   @Override
   public Future<T> submit(final Runnable task, T result) {
      QueueingTask futureTask = new QueueingTask(task, result);
      return doSubmit(futureTask);
   }

   private Future<T> doSubmit(QueueingTask futureTask) {
      if (blocking) {
         try {
            semaphore.acquire();
         } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IllegalLifecycleStateException();
         }
         try {
            futureTask.run();
         } finally {
            semaphore.release();
         }
      } else {
         queue.add(futureTask);
         if (log.isTraceEnabled()) log.tracef("New task submitted, tasks in queue %d, available permits %d", queue.size(),
               semaphore.availablePermits());
         executeFront();
      }
      return futureTask;
   }

   private void executeFront() {
      while (!queue.isEmpty() && semaphore.tryAcquire()) {
         QueueingTask next = queue.poll();
         if (next != null) {
            // Execute the task, and it will release the permit when it finishes
            executor.execute(next);
            // Only execute one task, if there are other tasks and permits available, they will be scheduled
            // to be executed either by the threads that released the permits or by the threads that added
            // the tasks.
            return;
         } else {
            // Perform another iteration, in case someone adds a task and skips executing it just before
            // we release the permit
            semaphore.release();
         }
      }
   }

   @Override
   public Future<T> take() throws InterruptedException {
      return completionQueue.take();
   }

   @Override
   public Future<T> poll() {
      return completionQueue.poll();
   }

   @Override
   public Future<T> poll(long timeout, TimeUnit unit) throws InterruptedException {
      return completionQueue.poll(timeout, unit);
   }

   private class QueueingTask extends FutureTask<T> {

      public QueueingTask(Callable<T> task) {
         super(task);
      }

      public QueueingTask(Runnable runnable, Object result) {
         super(runnable, (T) result);
      }

      @Override
      public void run() {
         try {
            QueueingTask next = this;
            do {
               next.runInternal();

               // Don't run another task if the current task called startBackgroundTask()
               // and there are no more permits available
               if (semaphore.availablePermits() < 0)
                  break;
               next = queue.poll();
            } while (next != null);
         } finally {
            semaphore.release();

            // In case we just got a new task between queue.poll() and semaphore.release()
            if (!queue.isEmpty()) {
               executeFront();
            }
         }
      }

      private void runInternal() {
         try {
            if (log.isTraceEnabled()) log.tracef("Task started, tasks in queue %d, available permits %d", queue.size(), semaphore.availablePermits());
            super.run();
         } finally {
            completionQueue.offer(this);
            if (log.isTraceEnabled()) log.tracef("Task finished, tasks in queue %d, available permits %d", queue.size(), semaphore.availablePermits());
         }
      }
   }

   /**
    * Extend {@code Semaphore} to expose the {@code reducePermits(int)} method.
    */
   private static class CustomSemaphore extends Semaphore {
      CustomSemaphore(int permits) {
         super(permits);
      }

      void removePermit() {
         super.reducePermits(1);
      }
   }
}
