/*
* Instances of SmartInterval extend the functionality of `setInterval`, make it configurable
* and controllable by a public API.
*
* */

(() => {
  class SmartInterval {
    /**
      * @param { function } opts.callback Function to be called on each iteration (required)
      * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially
      * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this
      * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this
      *                         when the page is hidden
      * @param { integer } opts.incrementByFactorOf `currentInterval` is incremented by this factor
      * @param { boolean } opts.lazyStart Configure if timer is initialized on
      *                    instantiation or lazily
      * @param { boolean } opts.immediateExecution Configure if callback should
      *                    be executed before the first interval.
      */
    constructor(opts = {}) {
      this.cfg = {
        callback: opts.callback,
        startingInterval: opts.startingInterval,
        maxInterval: opts.maxInterval,
        hiddenInterval: opts.hiddenInterval,
        incrementByFactorOf: opts.incrementByFactorOf,
        lazyStart: opts.lazyStart,
        immediateExecution: opts.immediateExecution,
      };

      this.state = {
        intervalId: null,
        currentInterval: this.cfg.startingInterval,
        pageVisibility: 'visible',
      };

      this.initInterval();
    }
    /* public */

    start() {
      const cfg = this.cfg;
      const state = this.state;

      if (cfg.immediateExecution) {
        cfg.immediateExecution = false;
        cfg.callback();
      }

      state.intervalId = window.setInterval(() => {
        cfg.callback();

        if (this.getCurrentInterval() === cfg.maxInterval) {
          return;
        }

        this.incrementInterval();
        this.resume();
      }, this.getCurrentInterval());
    }

    // cancel the existing timer, setting the currentInterval back to startingInterval
    cancel() {
      this.setCurrentInterval(this.cfg.startingInterval);
      this.stopTimer();
    }

    onVisibilityHidden() {
      if (this.cfg.hiddenInterval) {
        this.setCurrentInterval(this.cfg.hiddenInterval);
        this.resume();
      } else {
        this.cancel();
      }
    }

    // start a timer, using the existing interval
    resume() {
      this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
      this.start();
    }

    onVisibilityVisible() {
      this.cancel();
      this.start();
    }

    destroy() {
      this.cancel();
      document.removeEventListener('visibilitychange', this.handleVisibilityChange);
      $(document).off('visibilitychange').off('beforeunload');
    }

    /* private */

    initInterval() {
      const cfg = this.cfg;

      if (!cfg.lazyStart) {
        this.start();
      }

      this.initVisibilityChangeHandling();
      this.initPageUnloadHandling();
    }

    initVisibilityChangeHandling() {
      // cancel interval when tab no longer shown (prevents cached pages from polling)
      document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
    }

    initPageUnloadHandling() {
      // TODO: Consider refactoring in light of turbolinks removal.
      // prevent interval continuing after page change, when kept in cache by Turbolinks
      $(document).on('beforeunload', () => this.cancel());
    }

    handleVisibilityChange(e) {
      this.state.pageVisibility = e.target.visibilityState;
      const intervalAction = this.isPageVisible() ?
        this.onVisibilityVisible :
        this.onVisibilityHidden;

      intervalAction.apply(this);
    }

    getCurrentInterval() {
      return this.state.currentInterval;
    }

    setCurrentInterval(newInterval) {
      this.state.currentInterval = newInterval;
    }

    incrementInterval() {
      const cfg = this.cfg;
      const currentInterval = this.getCurrentInterval();
      if (cfg.hiddenInterval && !this.isPageVisible()) return;
      let nextInterval = currentInterval * cfg.incrementByFactorOf;

      if (nextInterval > cfg.maxInterval) {
        nextInterval = cfg.maxInterval;
      }

      this.setCurrentInterval(nextInterval);
    }

    isPageVisible() { return this.state.pageVisibility === 'visible'; }

    stopTimer() {
      const state = this.state;

      state.intervalId = window.clearInterval(state.intervalId);
    }
  }
  gl.SmartInterval = SmartInterval;
})(window.gl || (window.gl = {}));
