var mocks = require('mocks');

var timers = {};
// Keep a fake timestamp
// move on the time when runTimersToTime() is called
var now = 0;

// add a timer of type either 'timeout' or 'interval'
function _setTimer(type, callback, delay) {
  var token = null;
  do {
    token = Math.floor(Math.random() * 4294967296) + 1;
  } while (timers[token]);
  timers[token] = {
    type: type,
    callback: callback,
    // Add some random msecs to the delay to mimic code execution time.
    time: now + delay + Math.floor(Math.random() * 5),
    interval: (type == 'interval' ? delay : 0)
  };
  return token;
}

// clear a timer of type either 'timeout' or 'interval'
function _clearTimer(type, token) {
  if (timers[token] && timers[token].type == type) {
    delete timers[token];
  }
}

function _setTimeout(callback, delay) {
  return _setTimer('timeout', callback, delay);
}

function _setInterval(callback, delay) {
  return _setTimer('interval', callback, delay);
}

function _clearTimeout(token) {
  _clearTimer('timeout', token);
}

function _clearInterval(token) {
  _clearTimer('interval', token);
}

// Run timer of given token
function _runTimer(token) {
  // Skip non-existing token
  if (timers[token]) {
    if (timers[token].type == 'timeout') {
      // for 'timeout', run callback and delete the timer
      var callback = timers[token].callback;
      delete timers[token];
      callback();
    } else if (timers[token].type == 'interval') {
      // for 'interval', run callback and set the next invoke time
      // Add some random msecs to the delay to mimic code execution time.
      timers[token].time =
        now + timers[token].interval + Math.floor(Math.random() * 5);
      timers[token].callback();
    }
  }
}

function _runTimersOnce() {
  // Invoke all timers once regardsless of the delay
  for (var token in timers) {
    _runTimer(token);
  }
}

function _runTimersToTime(delay) {
  var toRunToken = _getNextTimerToken();
  if (!toRunToken) {
    return;
  }

  var minTime = timers[toRunToken].time;
  if (now + delay < minTime) {
    // Termination when there's no more timers to invoke
    now += delay;
  } else {
    // Recursively invoke the next to-run timer
    delay -= (minTime - now);
    now = minTime;
    _runTimer(toRunToken);
    _runTimersToTime(delay);
  }
}

function _runTimersRepeatedly() {
  // Only run a generous 1000 timers and then bail, since we may have entered
  // a loop if we have more than that.
  var max_timers = 1000;

  var token;
  for (var ii = 0; ii < max_timers; ii++) {
    token = _getNextTimerToken();

    if (!token) {
      break;
    }

    _runTimer(token);
  }

  if (ii === max_timers) {
    throw new Error("More timers still exist after " + max_timers + " timers!");
  }
}

function _clearTimers() {
  for (var token in timers) {
    delete timers[token];
  }
}

function _getNextTimerToken() {
  var nextTimerToken = null;
  var minTime = 31536000000; // One year
  // Find the next to invoke timer
  for (var token in timers) {
    if (timers[token].time < minTime) {
      nextTimerToken = token;
      minTime = timers[token].time;
    }
  }
  return nextTimerToken;
}

var mockTimers = {
  setTimeout: _setTimeout,
  clearTimeout: _clearTimeout,
  setInterval: _setInterval,
  clearInterval: _clearInterval,

  /**
 * Copyright 2013-2014, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
   * Iteratively run callbacks in time order during the time from now to
   * now + delay.
   * If one callback register another timer which should be run during now to
   * now + delay, the new timer will also be run in the right order.
   *
   * @param delay
   */
  runTimersToTime: _runTimersToTime,

  /**
   * Run all registered timer once. Newly registered timers will not be run.
   */
  runTimersOnce: _runTimersOnce,

  /**
   * Iteratively run callbacks until there are no timers left to call. Will
   * stop after a maximum number of iterations to avoid infinite loop.
   *
   * @param maximum iterations (optional)
   */
  runTimersRepeatedly: _runTimersRepeatedly,

  /**
   * Clear all timers
   */
  clearTimers: _clearTimers,

  /**
   * Get the number of remaining timers
   */
  getTimersCount: function() {
    return Object.keys(timers).length;
  }
};

module.exports.installMockTimers = function(window) {
  window._originalTimeouts = {
    setTimeout: window.setTimeout,
    clearTimeout: window.clearTimeout,
    setInterval: window.setInterval,
    clearInterval: window.clearInterval
  };
  window.setTimeout =
    mocks.getMockFunction().mockImplementation(mockTimers.setTimeout);
  window.clearTimeout =
    mocks.getMockFunction().mockImplementation(mockTimers.clearTimeout);
  window.setInterval =
    mocks.getMockFunction().mockImplementation(mockTimers.setInterval);
  window.clearInterval =
    mocks.getMockFunction().mockImplementation(mockTimers.clearInterval);
  window.mockRunTimersOnce =
    mocks.getMockFunction().mockImplementation(mockTimers.runTimersOnce);
  window.mockRunTimersToTime =
    mocks.getMockFunction().mockImplementation(mockTimers.runTimersToTime);
  window.mockRunTimersRepeatedly =
    mocks.getMockFunction().mockImplementation(mockTimers.runTimersRepeatedly);
  window.mockClearTimers =
    mocks.getMockFunction().mockImplementation(mockTimers.clearTimers);
  window.mockGetTimersCount =
    mocks.getMockFunction().mockImplementation(mockTimers.getTimersCount);
};

module.exports.uninstallMockTimers = function(window) {
  window.setTimeout = window._originalTimeouts.setTimeout;
  window.clearTimeout = window._originalTimeouts.clearTimeout;
  window.setInterval = window._originalTimeouts.setInterval;
  window.clearInterval = window._originalTimeouts.clearInterval;
  window._originalTimeouts = undefined;
  window.mockRunTimersOnce = undefined;
  window.mockRunTimersToTime = undefined;
  window.mockRunTimersRepeatedly = undefined;
  window.mockClearTimers = undefined;
  window.mockGetTimersCount = undefined;
};
