export type RafCallback = (time: number, delta: number) => void;

interface CallbackEntry {
  id: symbol;
  callback: RafCallback;
}

/**
 * RafManager is a singleton class that manages a single requestAnimationFrame (rAF) loop.
 */
export class RafManager {
  private static instance: RafManager;
  private callbacks: Array<CallbackEntry>;
  private frame: number | null;
  private now: number;

  /**
   * Private constructor to get the singleton instance of RafManager and prevent
   * direct construction calls with the `new` operator.
   */
  private constructor() {
    this.callbacks = [];
    this.frame = null;
    this.now = performance.now();
  }

  /** The static method that controls the access to the singleton instance. */
  public static getInstance(): RafManager {
    if (!RafManager.instance) {
      RafManager.instance = new RafManager();
    }
    return RafManager.instance;
  }

  /** Registers a callback and starts the rAF loop. */
  public add(callback: RafCallback): symbol {
    const id = Symbol();
    this.callbacks.push({ id, callback });
    this.startRaf();
    return id;
  }

  /** Removes a previously registered callback by its unique identifier. */
  public remove(id: symbol): void {
    for (let i = 0; i < this.callbacks.length; i++) {
      if (this.callbacks[i].id === id) {
        // Swap the target element with the last element and then remove it
        this.callbacks[i] = this.callbacks[this.callbacks.length - 1];
        this.callbacks.pop();
        break;
      }
    }

    if (this.callbacks.length === 0 && this.frame) {
      cancelAnimationFrame(this.frame);
      this.frame = null;
    }
  }

  private startRaf(): void {
    if (!this.frame && this.callbacks.length > 0) {
      this.frame = requestAnimationFrame(this.animate);
    }
  }

  /**
   * The main rAF loop, which calls each registered callback at the given FPS rate.
   * It provides each callback the current time and time delta since the last frame.
   */
  private animate = (timestamp: number): void => {
    this.frame = requestAnimationFrame(this.animate);

    const delta = timestamp - this.now;
    this.now = timestamp;

    for (let i = 0; i < this.callbacks.length; i++) {
      this.callbacks[i].callback(timestamp, delta);
    }
  };
}
