/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.imagepipeline.testing;

import com.facebook.common.internal.Preconditions;
import com.facebook.infer.annotation.Nullsafe;

/** A queue that executes items according to the passage of time. */
@Nullsafe(Nullsafe.Mode.LOCAL)
public class ScheduledQueue {

  private final FakeClock clock;
  private final DeltaQueue<Runnable> deltaQueue = new DeltaQueue<Runnable>();
  private long now;
  private boolean isTicking;
  private long tickRemaining;

  public ScheduledQueue(FakeClock clock) {
    this.clock = clock;
    now = clock.now();
    clock.addListener(
        new FakeClock.OnTickListener() {
          @Override
          public void onTick() {
            ScheduledQueue.this.tick();
          }
        });
  }

  /**
   * Runs time forwards by a given duration, executing any commands scheduled for execution during
   * that time period, and any background tasks spawned by the scheduled tasks. Therefore, when a
   * call to tick returns, the executor will be idle.
   */
  private void tick() {
    long newNow = clock.now();
    Preconditions.checkState(!isTicking);
    isTicking = true;
    try {
      tickRemaining = newNow - now;
      now = newNow;
      do {
        tickRemaining = deltaQueue.tick(tickRemaining);
        runUntilIdle();
      } while (deltaQueue.isNotEmpty() && tickRemaining > 0);
      Preconditions.checkState(isTicking);
    } finally {
      isTicking = false;
      tickRemaining = 0;
    }
  }

  /** Runs all commands scheduled to be executed immediately but does not tick time forward. */
  public void runUntilIdle() {
    while (!isIdle()) {
      runNextPendingCommand();
    }
  }

  /**
   * Gets whether the queue is idle.
   *
   * @return whether there are no more items in the queue that are ready to be processed
   */
  public boolean isIdle() {
    return deltaQueue.isEmpty() || deltaQueue.delay() > 0;
  }

  /** Runs the next command scheduled to be executed immediately. */
  public void runNextPendingCommand() {
    Runnable runnable = deltaQueue.pop();
    runnable.run();
  }

  /**
   * Returns how much time left until next command.
   *
   * <p>If there are no pending commands left will throw and exception.
   */
  public long getNextPendingCommandDelay() {
    if (getPendingCount() == 0) {
      throw new IllegalStateException();
    }
    return deltaQueue.delay();
  }

  /**
   * Adds a task to the queue that is ready to be executed.
   *
   * @param runnable the task to execute
   */
  public void add(Runnable runnable) {
    deltaQueue.add(tickRemaining, runnable);
  }

  /**
   * Adds a task to the queue that is to be executed to after the specified delay.
   *
   * @param runnable the task to execute
   * @param delayMs the delay before the task should be executed
   */
  public void add(Runnable runnable, long delayMs) {
    deltaQueue.add(tickRemaining + delayMs, runnable);
  }

  /**
   * Removes a task from the queue
   *
   * @param runnable The runnable to remove
   * @return whether the element was removed
   */
  public boolean remove(Runnable runnable) {
    return deltaQueue.remove(runnable);
  }

  /**
   * Gets the count of queued runnables, regardless of whether they are ready to be run.
   *
   * @return the count of queued runnables
   */
  public int getPendingCount() {
    return deltaQueue.size();
  }
}
