// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

/**
 * This class tries to enforce a state machine that looks something like this:
 *
 * .--------------------.  called   .-----------.  called  .---------------------.
 * |                    | --------> |           | -------> |                     |
 * | Nothing is running |           | 1 running |          | 1 running, 1 queued |
 * |                    | <-------- |           | <------- |                     |
 * '--------------------'   done    '-----------'   done   '---------------------'
 *                                                             |           ^
 *                                                             '-----------'
 *                                                                called
 *
 * Most notably, if something is queued and the function is called again, we discard the
 *   previously queued task completely.
 */
export class LatestQueue {
  private isRunning: boolean;

  private queuedTask?: () => Promise<void>;

  private onceEmptyCallbacks: Array<() => unknown>;

  constructor() {
    this.isRunning = false;
    this.onceEmptyCallbacks = [];
  }

  /**
   * Does one of the following:
   *
   * 1. Runs the task immediately.
   * 2. Enqueues the task, destroying any previously-enqueued task. In other words, 0 or 1
   *    tasks will be enqueued at a time.
   */
  add(task: () => Promise<void>): void {
    if (this.isRunning) {
      this.queuedTask = task;
    } else {
      this.isRunning = true;
      task().finally(() => {
        this.isRunning = false;

        const { queuedTask } = this;
        if (queuedTask) {
          this.queuedTask = undefined;
          this.add(queuedTask);
        } else {
          try {
            this.onceEmptyCallbacks.forEach(callback => {
              callback();
            });
          } finally {
            this.onceEmptyCallbacks = [];
          }
        }
      });
    }
  }

  /**
   * Adds a callback to be called the first time the queue goes from "running" to "empty".
   */
  onceEmpty(callback: () => unknown): void {
    this.onceEmptyCallbacks.push(callback);
  }
}
