export default class TaskRunner {
  constructor(concurrencyLimit = 1, total) {
    this._queue = [];
    this._index = 0;
    this._running = 0;
    this._complete = 0;
    this._concurrencyLimit = concurrencyLimit;

    if (total) {
      this._total = total;
      this._responses = new Array(total).fill(undefined);
    }

    this._onComplete = new Promise((resolve, reject) => {
      this._onCompleteResolve = resolve;
      this._onCompleteReject = reject;
    });
  }

  execute() {
    const isBusy = this._running >= this._concurrencyLimit || this._queue.length === 0;
    if (isBusy || this._paused) {
      return;
    }

    const task = this._queue.shift();
    this._running += 1;
    task(this._index);
    this._index += 1;
  }

  async push(fn) {
    return new Promise((resolve, reject) => {
      const task = async (index) => {
        try {
          const response = await fn();
          if (this._responses) {
            this._responses[index] = response;
          }
          resolve(response);
        } catch (error) {
          if (this._responses) {
            this._responses[index] = new Error(`Task was rejected: ${error}`);
          }
          reject(error);
        } finally {
          this._running -= 1;
          this._complete += 1;
          if (this._complete === this._total) {
            this._onCompleteResolve(this._responses);
          }
          this.execute();
        }
      }

      this._queue.push(task);
      this.execute();
    });
  }

  async onComplete() {
    if (!this._total) {
      this._onCompleteReject(new Error("Total is required to determine onComplete."));
    }
    return this._onComplete;
  }

  cancel() {
    this._concurrencyLimit = 0;
    this._onCompleteReject(this._responses);
  }
}