import taskQueue from "../../utils/taskQueue";
import { cancelIdleCb, reqIdleCb } from "./IdleDeadline";

const DEFAULT_MIN_TASK_TIME = 0;




/**
 * a queue system to utilize browser idle time to run tasks.
 * @author: Aamir khan
 */
class IdliQueue {

  constructor({
    ensureTasksRun = false,
    defaultMinTaskTime = DEFAULT_MIN_TASK_TIME,
  } = {}) {
    this.idliHandle = null;
    this.Queue_ = [];
    this.isProcessing_ = false;
    this.state_ = null;
    this.defaultMinTaskTime_ = defaultMinTaskTime;
    this.ensureTasksRun_ = ensureTasksRun;

    // Bind methods
    this.runTasksImmediately = this.runTasksImmediately.bind(this);
    this.runTasks_ = this.runTasks_.bind(this);
    this.onVisibilityChange_ = this.onVisibilityChange_.bind(this);

    if (this.ensureTasksRun_) {
      // Note: Safari does not reliably fire the `pagehide` or `visibilitychange`
      // for now we are only using chrome devices so we can use `visibilitychange`
      global.addEventListener('visibilitychange', this.onVisibilityChange_, true);


    }
  }

  /**
   * @param {...*} args
   */
  pushTask(...args) {
    this.addTask_(Array.prototype.push, ...args);
  }

  /**
   * @param {...*} args
   */
  unshiftTask(...args) {
    this.addTask_(Array.prototype.unshift, ...args);
  }

  /**
   * Runs all scheduled tasks synchronously.
   */
  runTasksImmediately() {
    // By not passing a deadline, all tasks will be run sync.
    this.runTasks_();
  }

  /**
   * @return {boolean}
   */
  hasPendingTasks() {
    return this.Queue_.length > 0;
  }


  /**
   * Clears all pending tasks for the queue and stops any scheduled tasks
   * from running.
   */
  clearPendingTasks() {
    this.Queue_ = [];
    this.cancelScheduledRun_();
  }

  /**
   * Returns the state object for the currently running task. If no task is
   * running, null is returned.
   * @return {Object}
   */
  getState() {
    return this.state_;
  }

  /**
   * Destroys the instance by unregistering all added event listeners and
   * removing any overridden methods.
   */
  destroy() {
    this.Queue_ = [];
    this.cancelScheduledRun_();

    if (this.ensureTasksRun_) {
      global.removeEventListener('visibilitychange', this.onVisibilityChange_, true);
    }
  }

  /**
   * @param {!Function} arrayMethod Either the Array.prototype{push|shift}.
   * @param {!Function} task
   * @param {{minTaskTime: number}=} param1
   * @private
   */
  addTask_(arrayMethod, task, { minTaskTime = this.defaultMinTaskTime_ } = {}) {
    const state = {
      time: Date.now(),
      visibilityState: document.visibilityState,
    };

    arrayMethod.call(this.Queue_, { state, task, minTaskTime });

    this.scheduleTasksToRun_();
  }

  /**
   * Schedules the task queue to be processed. If the document is in the
   * hidden state, they queue is scheduled as a microtask so it can be run
   * in cases where a macrotask couldn't (like if the page is unloading). If
   * the document is in the visible state, `requestIdleCallback` is used.
   * @private
   */
  scheduleTasksToRun_() {
    if (this.ensureTasksRun_ && document.visibilityState === 'hidden') {
      taskQueue(this.runTasks_);
    } else {
      if (!this.idliHandle) {
        this.idliHandle = reqIdleCb(this.runTasks_);
      }
    }
  }

  /**
   * Runs as many tasks in the queue as it can before reaching the
   * deadline. If no deadline is passed, it will run all tasks.
   * If an `IdleDeadline` object is passed (as is with `requestIdleCallback`)
   * then the tasks are run until there's no time remaining, at which point
   * we yield to input or other script and wait until the next idle time.
   * @param {IdleDeadline=} deadline
   * @private
   */
  runTasks_(deadline = undefined) {
    this.cancelScheduledRun_();

    if (!this.isProcessing_) {
      this.isProcessing_ = true;

      // Process tasks until there's no time left or we need to yield to input.
      while (this.hasPendingTasks() &&
        !shouldWeYield(deadline, this.Queue_[0].minTaskTime)) {
        const { task, state } = this.Queue_.shift();

        this.state_ = state;
        task(state);
        this.state_ = null;
      }

      this.isProcessing_ = false;

      if (this.hasPendingTasks()) {
        // Schedule the rest of the tasks for the next idle time.
        this.scheduleTasksToRun_();
      }
    }
  }

  /**
   * Cancels any scheduled idle callback and removes the handler (if set).
   * @private
   */
  cancelScheduledRun_() {
    cancelIdleCb(this.idliHandle);
    this.idliHandle = null;
  }

  /**
   * A callback for the `visibilitychange` event that runs all pending
   * callbacks immediately if the document's visibility state is hidden.
   * @private
   */
  onVisibilityChange_() {
    if (document.visibilityState === 'hidden') {
      this.runTasksImmediately();
    }
  }
}

/**
 * Returns true if the IdleDealine object exists and the remaining time is
 * less or equal to than the minTaskTime. Otherwise returns false.
 * @param {IdleDeadline|undefined} deadline
 * @param {number} minTaskTime
 * @return {boolean}
 * @private
 */
const shouldWeYield = (deadline, minTaskTime) => {
  return deadline && deadline.timeRemaining() <= minTaskTime
};



export default IdliQueue;