/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';

module.metadata = {
  "stability": "stable"
};

const { CC, Cc, Ci } = require("chrome");
const { when: unload } = require("./system/unload");

const { TYPE_ONE_SHOT, TYPE_REPEATING_SLACK } = Ci.nsITimer;
const Timer = CC("@mozilla.org/timer;1", "nsITimer");
const timers = Object.create(null);
const threadManager = Cc["@mozilla.org/thread-manager;1"].
                      getService(Ci.nsIThreadManager);
const prefBranch = Cc["@mozilla.org/preferences-service;1"].
                    getService(Ci.nsIPrefService).
                    QueryInterface(Ci.nsIPrefBranch);

let MIN_DELAY = 4;
// Try to get min timeout delay used by browser.
try { MIN_DELAY = prefBranch.getIntPref("dom.min_timeout_value"); } finally {}


// Last timer id.
let lastID = 0;

// Sets typer either by timeout or by interval
// depending on a given type.
function setTimer(type, callback, delay, ...args) {
  let id = ++ lastID;
  let timer = timers[id] = Timer();
  timer.initWithCallback({
    notify: function notify() {
      try {
        if (type === TYPE_ONE_SHOT)
          delete timers[id];
        callback.apply(null, args);
      }
      catch(error) {
        console.exception(error);
      }
    }
  }, Math.max(delay || MIN_DELAY), type);
  return id;
}

function unsetTimer(id) {
  let timer = timers[id];
  delete timers[id];
  if (timer) timer.cancel();
}

let immediates = new Map();

let dispatcher = _ => {
  // Allow scheduling of a new dispatch loop.
  dispatcher.scheduled = false;
  // Take a snapshot of timer `id`'s that have being present before
  // starting a dispatch loop, in order to ignore timers registered
  // in side effect to dispatch while also skipping immediates that
  // were removed in side effect.
  let ids = [id for ([id] of immediates)];
  for (let id of ids) {
    let immediate = immediates.get(id);
    if (immediate) {
      immediates.delete(id);
      try { immediate(); }
      catch (error) { console.exception(error); }
    }
  }
}

function setImmediate(callback, ...params) {
  let id = ++ lastID;
  // register new immediate timer with curried params.
  immediates.set(id, _ => callback.apply(callback, params));
  // if dispatch loop is not scheduled schedule one. Own scheduler
  if (!dispatcher.scheduled) {
    dispatcher.scheduled = true;
    threadManager.currentThread.dispatch(dispatcher,
                                         Ci.nsIThread.DISPATCH_NORMAL);
  }
  return id;
}

function clearImmediate(id) {
  immediates.delete(id);
}

// Bind timers so that toString-ing them looks same as on native timers.
exports.setImmediate = setImmediate.bind(null);
exports.clearImmediate = clearImmediate.bind(null);
exports.setTimeout = setTimer.bind(null, TYPE_ONE_SHOT);
exports.setInterval = setTimer.bind(null, TYPE_REPEATING_SLACK);
exports.clearTimeout = unsetTimer.bind(null);
exports.clearInterval = unsetTimer.bind(null);

// all timers are cleared out on unload.
unload(function() {
  immediates.clear();
  Object.keys(timers).forEach(unsetTimer)
});
