"use strict";

const log = require("./log");
const pinkySwear = require("./pinkyswear");

exports.createNiceBackoffDelayFunc = function() {
  // delays, in seconds; recycle the last value as needed
  let niceBackoff = [0, 1, 2, 3, 5];
  let pos = -1;
  return function() {
    pos = Math.min(++pos, niceBackoff.length - 1);
    return niceBackoff[pos] * 1000;
  };
};

// Call a function that returns a promise one or more times, until
// it either returns successfully, or time expires. Use a configurable
// delay in between attempts.
//
// progressCallbacks should be an EventEmitter or similar; this function will
// emit the following events (and arguments):
//
// "schedule", delayMillis  // Called each time the next attempt is scheduled
// "attempt"                // Called each time an attempt begins
// "success"                // Called if retryPromise_p ends in success
// "failure"                // Called if retryPromise_p ends in failure
//
// On the same progressCallbacks object, this function will LISTEN FOR the
// following event (note that it can be invoked repeatedly):
//
// "retry-now"              // Stop waiting for next attempt; do it immediately.
//                          // If emitted during an attempt, event will be
//                          // ignored.
exports.retryPromise_p = function(create_p, delayFunc, expiration,
  progressCallbacks) {

  if (!progressCallbacks)
    progressCallbacks = {emit: () => {}};

  let promise = exports.promise();

  let delay = delayFunc();
  // Don't let the delay exceed the remaining time til expiration.
  delay = Math.min(delay, expiration - Date.now());
  // But in no case should the delay be less than zero, either.
  delay = Math.max(0, delay);

  function attempt() {
    progressCallbacks.removeListener("retry-now", retryNow);
    progressCallbacks.emit("attempt");

    create_p().then(
      function(value) {
        progressCallbacks.emit("success");
        promise(true, [value]);
      },
      function(err) {
        if (Date.now() >= expiration) {
          progressCallbacks.emit("failure");
          promise(false, [err]);
        } else {
          // Recurse. pinkySwear doesn't give us a way to easily
          // resolve a promise with another promise, so we have to
          // do it manually.
          exports.retryPromise_p(create_p, delayFunc, expiration, progressCallbacks).then(
            function() { promise(true, arguments); },
            function() { promise(false, arguments); }
          ).done();
        }
      }
    ).done();
  }

  let timeoutHandle = setTimeout(attempt, delay);

  function retryNow() {
    clearTimeout(timeoutHandle);
    attempt();
  }
  progressCallbacks.on("retry-now", retryNow);

  progressCallbacks.emit("schedule", delay);

  return promise;
};

exports.createEvent = function(type, props) {
  if (global.document) {
    try {
      return new Event(type, props);
    } catch(e) {
      // "new Event()" not supported in MSIE, not even 11
      let evt = global.document.createEvent("Event");
      evt.initEvent(type, true, false);
      for (let key in props) {
        evt[key] = props[key];
      }
      return evt;
    }
  } else if (props) {
    props.type = type;
    return props;
  } else {
    return {type: type};
  }
};

function addDone(prom) {
  prom.done = function() {
    prom.then(null, function(err) {
      log("Unhandled promise error: " + err);
      log(err.stack);
    });
  };
  return prom;
}
exports.promise = function() {
  return pinkySwear(addDone);
};

// PauseConnection is similar to pauseable streams
// in Node.js; used to delay events in case the
// process of registering event handlers has some
// asynchronicity to it. In our case, returning a
// connection as a promise means that whoever is
// waiting on the promise won't get a chance to
// register event listeners until at least one trip
// through the event loop.
//
// PauseConnection instances start out in the paused
// state, and must be manually resumed with .resume().
// The readyState is also paused, to ensure that it
// is in sync with the events (i.e. it'd be weird to
// see a connection transition to OPEN without onopen)
// but it is debatable whether this is more correct
// than having readyState always reflect the actual
// state of the underlying connection, because the
// underlying state is more relevant when it comes to
// calling send()/close() on this connection (which
// pass straight through to the underlying connection).
exports.PauseConnection = PauseConnection;
function PauseConnection(conn) {
  this._conn = conn;
  this._paused = true;
  this._events = [];
  this._timeout = null;
  this.readyState = conn.readyState;

  let pauseConnection = this;
  ["onopen", "onmessage", "onerror", "onclose"].forEach(evt => {
    conn[evt] = function() {
      if (pauseConnection._paused) {
        pauseConnection._events.push({event: evt, args: arguments, readyState: conn.readyState});
      } else {
        this.readyState = conn.readyState;
        pauseConnection[evt].apply(this, arguments);
      }
    };
  });
}

PauseConnection.prototype.resume = function() {
  this._timeout = setTimeout(() => {
    while (this._events.length) {
      let e = this._events.shift();
      this.readyState = e.readyState;
      this[e.event].apply(this, e.args);
    }
    this._paused = false;
  }, 0);
};
PauseConnection.prototype.pause = function() {
  clearTimeout(this._timeout);
  this._paused = true;
};

PauseConnection.prototype.close = function() {
  this._conn.close.apply(this._conn, arguments);
};
PauseConnection.prototype.send = function() {
  this._conn.send.apply(this._conn, arguments);
};

Object.defineProperty(PauseConnection.prototype, "url", {
  get: function() {
    return this._conn.url;
  }
});
Object.defineProperty(PauseConnection.prototype, "protocol", {
  get: function() {
    return this._conn.protocol;
  }
});
Object.defineProperty(PauseConnection.prototype, "extensions", {
  get: function() {
    return this._conn.extensions;
  }
});
