import {user} from '#utils/log';

import {reportError} from '../error-reporting';
import {getMode} from '../mode';
import {
  registerServiceBuilder,
  registerServiceBuilderInEmbedWin,
} from '../service-helpers';

const TAG = 'timer';
let timersForTesting;

/**
 * Helper with all things Timer.
 */
export class Timer {
  /**
   * @param {!Window} win
   */
  constructor(win) {
    /** @const {!Window} */
    this.win = win;

    /** @private @const {!Promise}  */
    this.resolved_ = this.win.Promise.resolve();

    this.taskCount_ = 0;

    this.canceled_ = {};

    /** @const {number} */
    this.startTime_ = Date.now();
  }

  /**
   * Returns time since start in milliseconds.
   * @return {number}
   */
  timeSinceStart() {
    return Date.now() - this.startTime_;
  }

  /**
   * Runs the provided callback after the specified delay. This uses a micro
   * task for 0 or no specified time. This means that the delay will actually
   * be close to 0 and this will NOT yield to the event queue.
   *
   * Returns the timer ID that can be used to cancel the timer (cancel method).
   * @param {function()} callback
   * @param {number=} opt_delay
   * @return {number|string}
   */
  delay(callback, opt_delay) {
    if (!opt_delay) {
      // For a delay of zero,  schedule a promise based micro task since
      // they are predictably fast.
      const id = 'p' + this.taskCount_++;
      this.resolved_
        .then(() => {
          if (this.canceled_[id]) {
            delete this.canceled_[id];
            return;
          }
          callback();
        })
        .catch(reportError);
      return id;
    }
    const wrapped = () => {
      try {
        callback();
      } catch (e) {
        reportError(e);
        throw e;
      }
    };
    const index = this.win.setTimeout(wrapped, opt_delay);
    if (getMode().test) {
      if (!timersForTesting) {
        timersForTesting = [];
      }
      timersForTesting.push(index);
    }
    return index;
  }

  /**
   * Cancels the previously scheduled callback.
   * @param {number|string|null} timeoutId
   */
  cancel(timeoutId) {
    if (typeof timeoutId == 'string') {
      this.canceled_[timeoutId] = true;
      return;
    }
    this.win.clearTimeout(timeoutId);
  }

  /**
   * Returns a promise that will resolve after the delay. Optionally, the
   * resolved value can be provided as opt_result argument.
   * @param {number=} opt_delay
   * @return {!Promise}
   */
  promise(opt_delay) {
    return new this.win.Promise((resolve) => {
      // Avoid wrapping in closure if no specific result is produced.
      const timerKey = this.delay(resolve, opt_delay);
      if (timerKey == -1) {
        throw new Error('Failed to schedule timer.');
      }
    });
  }

  /**
   * Returns a promise that will fail after the specified delay. Optionally,
   * this method can take opt_racePromise parameter. In this case, the
   * resulting promise will either fail when the specified delay expires or
   * will resolve based on the opt_racePromise, whichever happens first.
   * @param {number} delay
   * @param {?Promise<RESULT>|undefined} opt_racePromise
   * @param {string=} opt_message
   * @return {!Promise<RESULT>}
   * @template RESULT
   */
  timeoutPromise(delay, opt_racePromise, opt_message) {
    let timerKey;
    const delayPromise = new this.win.Promise((_resolve, reject) => {
      timerKey = this.delay(() => {
        reject(user().createError(opt_message || 'timeout'));
      }, delay);

      if (timerKey == -1) {
        throw new Error('Failed to schedule timer.');
      }
    });
    if (!opt_racePromise) {
      return delayPromise;
    }
    const cancel = () => {
      this.cancel(timerKey);
    };
    opt_racePromise.then(cancel, cancel);
    return this.win.Promise.race([delayPromise, opt_racePromise]);
  }

  /**
   * Returns a promise that resolves after `predicate` returns true.
   * Polls with interval `delay`
   * @param {number} delay
   * @param {function():boolean} predicate
   * @return {!Promise}
   */
  poll(delay, predicate) {
    return new this.win.Promise((resolve) => {
      const interval = this.win.setInterval(() => {
        if (predicate()) {
          this.win.clearInterval(interval);
          resolve();
        }
      }, delay);
    });
  }
}

/**
 * @param {!Window} window
 */
export function installTimerService(window) {
  registerServiceBuilder(window, TAG, Timer);
}

/**
 * @param {!Window} embedWin
 */
export function installTimerInEmbedWindow(embedWin) {
  registerServiceBuilderInEmbedWin(embedWin, TAG, Timer);
}

/**
 * Cancels all timers scheduled during the current test
 */
export function cancelTimersForTesting() {
  if (!timersForTesting) {
    return;
  }
  timersForTesting.forEach(clearTimeout);
  timersForTesting = null;
}
