package org.threadly.concurrent;

import java.util.concurrent.atomic.AtomicInteger;

import org.threadly.util.ArgumentVerifier;

/**
 * Abstract implementation for more complicated recurring behavior.  Unlike submitting a task to 
 * {@link SubmitterScheduler#scheduleWithFixedDelay(Runnable, long, long)}, this can provide the 
 * ability to only have the task scheduled if there is work to do.  In addition it provides the 
 * ability to change the frequency of execution without needing to remove and re-add the task.
 * <p>
 * This task will only schedule or reschedule itself if it has been notified there is work to do.  
 * It is assumed that after execution all work is complete.  If there is additional work to 
 * perform, then the task should invoke {@link #signalToRun()} before it completes to ensure that 
 * it is rescheduled at the current set delay.  Because of that behavior there is no way to remove 
 * this task from the scheduler, instead you must just ensure that {@link #signalToRun()} is not 
 * invoked to prevent the task from rescheduling itself.
 * <p>
 * An additional advantage to using this over scheduling a recurring task is that you don't have 
 * to worry about removing the task before garbage collection occurs (ie no cleanup, just stop 
 * invoking {@link #signalToRun()}).
 * 
 * @since 4.9.0
 */
public abstract class ReschedulingOperation {
  private final SubmitterScheduler scheduler;
  // -1 = not scheduled, 0 = scheduled, 1 = running, 2 = updated while running
  private final AtomicInteger taskState;
  private final CheckRunner runner;
  private volatile long scheduleDelay;
  
  /**
   * Construct a new operation with the provided scheduler to schedule on to and the initial delay.
   * 
   * @param scheduler Scheduler to execute on.
   * @param scheduleDelay Delay in milliseconds to schedule operation out when has stuff to do
   */
  protected ReschedulingOperation(SubmitterScheduler scheduler, long scheduleDelay) {
    ArgumentVerifier.assertNotNull(scheduler, "scheduler");
    
    this.scheduler = scheduler;
    this.taskState = new AtomicInteger(-1);
    this.runner = new CheckRunner();
    setScheduleDelay(scheduleDelay);
  }

  /**
   * Check to see if this operation is either currently running, or scheduled to run.
   * 
   * @return {@code true} means this operation still has things to do
   */
  public boolean isActive() {
    return taskState.get() != -1;
  }
  
  /**
   * Adjusts the delay at which this task is scheduled or rescheduled.  This can be invoked during 
   * {@link #run()} to change how long till it executes next.
   * 
   * @param scheduleDelay Delay in milliseconds to schedule operation out on, can not be negative
   */
  public void setScheduleDelay(long scheduleDelay) {
    ArgumentVerifier.assertNotNegative(scheduleDelay, "scheduleDelay");
    
    this.scheduleDelay = scheduleDelay;
  }
  
  /**
   * Invoke to indicate that this operation has stuff to do.  If necessary the task will be 
   * scheduled for execution.  If the task is already running then it will ensure the task 
   * re-executes itself when done (at the set delay).  This re-execution can help ensure that any 
   * thread state changes can be witnessed on the next execution.
   */
  public void signalToRun() {
    while (true) {
      if (taskState.get() == -1) {
        if (taskState.compareAndSet(-1, 0)) {
          scheduler.schedule(runner, scheduleDelay);
          return;
        }
      } else if (taskState.get() == 1) {
        if (taskState.compareAndSet(1, 2)) {
          return;
        }
      } else {
        // either already scheduled, or already marked as more added
        return;
      }
    }
  }
  
  /**
   * Abstract function which must be implemented to handle actual operation.  It is expected that 
   * when this runs all outstanding work is handled.  If it can not be fully handled then invoke 
   * {@link #signalToRun()} before returning.
   * <p>
   * If this throws an exception it will not impact the state of future executions (ie if 
   * {@link #signalToRun()} was invoked, the task will be rescheduled despite a thrown exception).
   */
  protected abstract void run();
  
  /**
   * Class to in a thread safe way update the execution state, and reschedule the task on 
   * completion if necessary.
   * 
   * @since 4.9.0
   */
  protected class CheckRunner implements Runnable {
    @Override
    public void run() {
      taskState.set(1);

      try {
        ReschedulingOperation.this.run();
      } finally {
        while (true) {
          if (taskState.get() == 1) {
            if (taskState.compareAndSet(1, -1)) {
              // nothing to run, just return
              break;
            }
          } else if (taskState.get() == 2) { // will be set back to 1 when this restarts
            scheduler.schedule(this, scheduleDelay);
            break;
          }
        }
      }
    }
    
    @Override
    public String toString() {
      return "CheckRunner for: " + ReschedulingOperation.this.toString();
    }
  }
}

@@ -3300,27 +3300,38 @@
 e) %7B%0A      i
-f (
+nt casState = 
 taskState.ge
@@ -3329,24 +3329,44 @@
 kState.get()
+;%0A      if (casState
  == -1) %7B%0A  
@@ -3503,39 +3503,32 @@
  %7D else if (
-t
+c
 as
-k
 State
-.get()
  == 1) %7B%0A   

Fix for potential state change durring check