/*
 * Promy: Promises library
 * Copyright (C) 2013  Nexedi SA
 *
 *   This library is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU Lesser General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This library is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU Lesser General Public License for more details.
 *
 *   You should have received a copy of the GNU Lesser General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global setInterval, setTimeout, clearInterval, clearTimeout */

(function (dependencies, module) {
  "use strict";
  /*global define, exports, window */
  if (typeof define === 'function' && define.amd) {
    return define(dependencies, module);
  }
  if (typeof exports === 'object') {
    module(exports);
  }
  if (typeof window === 'object') {
    window.promy = {};
    module(window.promy);
  }
}(['exports'], function (exports) {
  "use strict";

  var UNRESOLVED = 0, RESOLVED = 1, REJECTED = 2, CANCELLED = 3;

  /**
   * thenItem(item, [onSucess], [onError], [onProgress]): any
   *
   * Execute one of the given callback when the item is fulfilled. If the item
   * is not a promise, then onSuccess is called with the item as first
   * parameter.
   *
   * @param  {Any} item A promise, deferred or a simple value
   * @param  {Function} [onSuccess] The callback to call on resolve
   * @param  {Function} [onError] The callback to call on reject
   * @param  {Function} [onProgress] The callback to call on notify
   */
  function thenItem(item, onSuccess, onError, onProgress) {
    if (typeof item === 'object' && item !== null) {
      if (typeof item.promise === 'object' && item.promise !== null &&
          typeof item.promise.then === 'function') {
        // item seams to be a Deferred
        return item.promise.then(
          onSuccess,
          onError,
          onProgress
        );
      }
      if (typeof item.then === 'function') {
        // item seams to be a Promise
        return item.then(
          onSuccess,
          onError,
          onProgress
        );
      }
    }
    return onSuccess(item);
  }

  /**
   * promiseResolve(promise, answers): any
   *
   * Resolve the promise with the given answers.
   *
   * @param  {Promise} promise The promise to resolve
   * @param  {Array} answers The arguments to give
   */
  function promiseResolve(promise, answers) {
    var array;
    if (promise._state === UNRESOLVED) {
      promise._state = RESOLVED;
      promise._answers = answers;
      array = promise._onResolve.slice();
      setTimeout(function () {
        var i;
        for (i = 0; i < array.length; i += 1) {
          try {
            array[i].apply(promise, promise._answers);
          } catch (ignore) {} // errors will never be retrieved by global
        }
      });
      // free the memory
      promise._onResolve = undefined;
      promise._onReject = undefined;
      promise._onProgress = undefined;
    }
  }

  /**
   * promiseReject(promise, answers): any
   *
   * Reject the promise with the given answers.
   *
   * @param  {Promise} promise The promise to reject
   * @param  {Array} answers The arguments to give
   */
  function promiseReject(promise, answers) {
    var array;
    if (promise._state === UNRESOLVED) {
      promise._state = REJECTED;
      promise._answers = answers;
      array = promise._onReject.slice();
      setTimeout(function () {
        var i;
        for (i = 0; i < array.length; i += 1) {
          try {
            array[i].apply(promise, promise._answers);
          } catch (ignore) {} // errors will never be retrieved by global
        }
      });
      // free the memory
      promise._onResolve = undefined;
      promise._onReject = undefined;
      promise._onProgress = undefined;
    }
  }

  /**
   * promiseNotify(promise, answers): any
   *
   * Notify the promise with the given answers.
   *
   * @param  {Promise} promise The promise to notify
   * @param  {Array} answers The arguments to give
   */
  function promiseNotify(promise, answers) {
    var i;
    if (promise._onProgress) {
      for (i = 0; i < promise._onProgress.length; i += 1) {
        try {
          promise._onProgress[i].apply(promise, answers);
        } catch (ignore) {} // errors will never be retrieved by global
      }
    }
  }

  /**
   * Promise(cancel)
   *
   * @class Promise
   * @constructor
   */
  function Promise(cancel) {
    this._onReject = [];
    this._onResolve = [];
    this._onProgress = [];
    this._state = UNRESOLVED;
    if (typeof cancel === 'function') {
      this.promise._cancel = cancel;
    }
  }

  ////////////////////////////////////////////////////////////
  // http://wiki.commonjs.org/wiki/Promises/A
  // then(fulfilledHandler, errorHandler, progressHandler)

  /**
   * then([onSuccess], [onError], [onProgress]): Promise
   *
   * Returns a new Promise with the return value of the `onSuccess` or `onError`
   * callback as first parameter. If the pervious promise is resolved, the
   * `onSuccess` callback is called. If rejected, the `onError` callback is
   * called. If notified, `onProgress` is called.
   *
   *     Deferred.when(1).
   *       then(function (one) { return one + 1; }).
   *       then(console.log); // shows 2
   *
   * @method then
   * @param  {Function} [onSuccess] The callback to call on resolve
   * @param  {Function} [onError] The callback to call on reject
   * @param  {Function} [onProgress] The callback to call on notify
   * @return {Promise} The new promise
   */
  Promise.prototype.then = function (onSuccess, onError, onProgress) {
    /*global Deferred*/
    var defer, next = new this.constructor(this._cancel), that = this;
    defer = new Deferred();
    defer.promise = next;
    switch (this._state) {
    case RESOLVED:
      if (typeof onSuccess === 'function') {
        setTimeout(function () {
          try {
            thenItem(
              onSuccess.apply(that, that._answers),
              defer.resolve.bind(defer),
              defer.reject.bind(defer)
            );
          } catch (e) {
            defer.reject(e);
          }
        });
      } else {
        setTimeout(function () {
          defer.resolve.apply(defer, that._answers);
        });
      }
      break;
    case REJECTED:
      if (typeof onError === 'function') {
        setTimeout(function () {
          var result;
          try {
            result = onError.apply(that, that._answers);
            if (result === undefined) {
              return defer.reject.apply(defer, that._answers);
            }
            thenItem(
              result,
              defer.reject.bind(defer),
              defer.reject.bind(defer)
            );
          } catch (e) {
            defer.reject(e);
          }
        });
      } else {
        setTimeout(function () {
          defer.reject.apply(defer, that._answers);
        });
      }
      break;
    case UNRESOLVED:
      if (typeof onSuccess === 'function') {
        this._onResolve.push(function () {
          try {
            thenItem(
              onSuccess.apply(that, arguments),
              defer.resolve.bind(defer),
              defer.reject.bind(defer),
              defer.notify.bind(defer)
            );
          } catch (e) {
            defer.reject(e);
          }
        });
      } else {
        this._onResolve.push(function () {
          defer.resolve.apply(defer, arguments);
        });
      }
      if (typeof onError === 'function') {
        this._onReject.push(function () {
          try {
            thenItem(
              onError.apply(that, that._answers),
              defer.reject.bind(defer),
              defer.reject.bind(defer)
            );
          } catch (e) {
            defer.reject(e);
          }
        });
      } else {
        this._onReject.push(function () {
          defer.reject.apply(defer, that._answers);
        });
      }
      if (typeof onProgress === 'function') {
        this._onProgress.push(function () {
          var result;
          try {
            result = onProgress.apply(that, arguments);
            if (result === undefined) {
              defer.notify.apply(defer, arguments);
            } else {
              defer.notify(result);
            }
          } catch (e) {
            defer.notify.apply(defer, arguments);
          }
        });
      } else {
        this._onProgress.push(function () {
          defer.notify.apply(defer, arguments);
        });
      }
      break;
    default:
      break;
    }
    return next;
  };

  // p.resolve() ?
  // p.reject() ?
  // p.notify() ?

  /**
   * p.cancel(): p
   *
   * Cancels the operation by calling promise._cancel().
   *
   * @method cancel
   * @return {Promise} this
   */
  Promise.prototype.cancel = function () {
    if (this._state === UNRESOLVED) {
      this._state = CANCELLED;
      if (typeof this._cancel === 'function') {
        this._cancel();
      }
      this._onResolve = undefined;
      this._onReject = undefined;
      this._onProgress = undefined;
    }
    return this;
  };

  /**
   * p.timeout(delay): p
   *
   * Reject the promise with an Error("Timeout") and cancel the operation.
   *
   * @method timeout
   * @param  {Number} delay The delay before rejection
   * @return {Promise} this
   */
  Promise.prototype.timeout = function (delay) {
    return exports.choose(this, exports.delay(delay).then(function () {
      throw new Error("Timeout (" + delay + ")");
    }));
  };

  ////////////////////////////////////////////////////////////
  // http://wiki.commonjs.org/wiki/Promises/A
  // get(propertyName)

  /**
   * get(property): Promise
   *
   * Get the property of the promise response as first parameter of the new
   * Promise.
   *
   *     Deferred.when({'a': 'b'}).get('a').then(console.log); // shows 'b'
   *
   * @method get
   * @param  {String} property The object property name
   * @return {Promise} The promise
   */
  Promise.prototype.get = function (property) {
    return this.then(function (dict) {
      return dict[property];
    });
  };

  ////////////////////////////////////////////////////////////
  // http://wiki.commonjs.org/wiki/Promises/A
  // call(functionName, arg1, arg2, ...)

  /**
   * call(function_name, *args): Promise
   *
   *     Deferred.when({'a': console.log}).call('a', 'b'); // shows 'b'
   *
   * @method call
   * @param  {String} function_name The function to call
   * @param  {Any} *[args] The function arguments
   * @return {Promise} A new promise
   */
  Promise.prototype.call = function (function_name) {
    var args = Array.prototype.slice.call(arguments, 1);
    return this.then(function (dict) {
      return dict[function_name].apply(dict, args);
    });
  };

  /**
   * put(property, value): Promise
   *
   * Put a property value from a promise response and return the registered
   * value as first parameter of the new Promise.
   *
   *     Deferred.when({'a': 'b'}).put('a', 'c').then(console.log); // shows 'c'
   *
   * @method put
   * @param  {String} property The object property name
   * @param  {String} value The value to put
   * @return {Promise} A new promise
   */
  Promise.prototype.put = function (property, value) {
    return this.then(function (dict) {
      dict[property] = value;
      return dict[property];
    });
  };

  /**
   * p.done(callback): p
   *
   * Call the callback on resolve.
   *
   *     Deferred.when(1).
   *       done(function (one) { return one + 1; }).
   *       done(console.log); // shows 1
   *
   * @method done
   * @param  {Function} callback The callback to call on resolve
   * @return {Promise} This promise
   */
  Promise.prototype.done = function (callback) {
    var that = this;
    if (typeof callback !== 'function') {
      return this;
    }
    switch (this._state) {
    case RESOLVED:
      setTimeout(function () {
        try {
          callback.apply(that, that._answers);
        } catch (ignore) {} // errors will never be retrieved by global
      });
      break;
    case UNRESOLVED:
      this._onResolve.push(callback);
      break;
    default:
      break;
    }
    return this;
  };

  /**
   * p.fail(callback): p
   *
   * Call the callback on reject.
   *
   *     promisedTypeError().
   *       fail(function (e) { name_error(); }).
   *       fail(console.log); // shows TypeError
   *
   * @method fail
   * @param  {Function} callback The callback to call on reject
   * @return {Promise} This promise
   */
  Promise.prototype.fail = function (callback) {
    var that = this;
    if (typeof callback !== 'function') {
      return this;
    }
    switch (this._state) {
    case REJECTED:
      setTimeout(function () {
        try {
          callback.apply(that, that._answers);
        } catch (ignore) {} // errors will never be retrieved by global
      });
      break;
    case UNRESOLVED:
      this._onReject.push(callback);
      break;
    default:
      break;
    }
    return this;
  };

  /**
   * p.progress(callback): p
   *
   * Call the callback on notify.
   *
   *     Promise.delay(100, 10).
   *       progress(function () { return null; }).
   *       progress(console.log); // does not show null
   *
   * @method progress
   * @param  {Function} callback The callback to call on notify
   * @return {Promise} This promise
   */
  Promise.prototype.progress = function (callback) {
    if (typeof callback !== 'function') {
      return this;
    }
    switch (this._state) {
    case UNRESOLVED:
      this._onProgress.push(callback);
      break;
    default:
      break;
    }
    return this;
  };

  /**
   * p.always(callback): p
   *
   * Call the callback on resolve or on reject.
   *
   *     sayHello().
   *       done(iAnswer).
   *       fail(iHeardNothing).
   *       always(iKeepWalkingAnyway);
   *
   * @method always
   * @param  {Function} callback The callback to call on resolve or on reject
   * @return {Promise} This promise
   */
  Promise.prototype.always = function (callback) {
    var that = this;
    if (typeof callback !== 'function') {
      return this;
    }
    switch (this._state) {
    case RESOLVED:
    case REJECTED:
      setTimeout(function () {
        try {
          callback.apply(that, that._answers);
        } catch (ignore) {} // errors will never be retrieved by global
      });
      break;
    case UNRESOLVED:
      that._onReject.push(callback);
      that._onResolve.push(callback);
      break;
    default:
      break;
    }
    return this;
  };

  exports.Promise = Promise;

  /**
   * Deferred(cancel)
   *
   * @class Deferred
   * @constructor
   */
  function Deferred(cancel) {
    this.promise = new Promise(cancel);
  }

  /**
   * resolve(*args): any
   *
   * Resolves the promise with the given arguments.
   *
   * @method resolve
   * @param  {Any} *[args] The arguments to give
   */
  Deferred.prototype.resolve = function () {
    return promiseResolve(this.promise, arguments);
  };

  /**
   * reject(*args): any
   *
   * Rejects the promise with the given arguments.
   *
   * @method reject
   * @param  {Any} *[args] The arguments to give
   */
  Deferred.prototype.reject = function () {
    return promiseReject(this.promise, arguments);
  };

  /**
   * notify(*args): any
   *
   * Notifies the promise with the given arguments.
   *
   * @method notify
   * @param  {Any} *[args] The arguments to give
   */
  Deferred.prototype.notify = function () {
    return promiseNotify(this.promise, arguments);
  };

  exports.Deferred = Deferred;

  //////////////////////////////////////////////////////////////////////
  // Inspired by Task.js

  /**
   * now(value): Promise
   *
   * Converts an ordinary value into a fulfilled promise.
   *
   * @param  {Any} value The value to use
   * @return {Promise} The resolved promise
   */
  exports.now = function (value) {
    var deferred = new Deferred();
    deferred.resolve(value);
    return deferred.promise;
  };

  /**
   * join(*promises): Promise
   *
   * Produces a promise that is resolved when all the given promises are
   * resolved. The resolved value is an array of each of the resolved values of
   * the given promises.
   *
   * If any of the promises is rejected, the joined promise is rejected with the
   * same error, and any remaining unfulfilled promises are cancelled.
   *
   * @param  {Promise} *[promises] The promises to join
   * @return {Promise} A new promise
   */
  exports.join = function () {
    var promises, results = [], i, count = 0, deferred;
    promises = Array.prototype.slice.call(arguments);
    function cancel() {
      var j;
      for (j = 0; j < promises.length; j += 1) {
        promises[j].cancel();
      }
    }
    deferred = new Deferred(cancel);
    function succeed(j) {
      return function (answer) {
        results[j] = answer;
        count += 1;
        if (count !== promises.length) {
          return;
        }
        deferred.resolve(results);
      };
    }
    function failed(answer) {
      cancel();
      deferred.reject(answer);
    }
    function notify(j) {
      return function (answer) {
        deferred.notify({
          "promise": this,
          "index": j,
          "answer": answer
        });
      };
    }
    for (i = 0; i < promises.length; i += 1) {
      promises[i].then(succeed(i), failed, notify(i));
    }
    return deferred.promise;
  };

  /**
   * choose(*promises): Promise
   *
   * Produces a promise that is fulfilled when any one of the given promises is
   * fulfilled. As soon as one of the promises is fulfilled, whether by being
   * resolved or rejected, all the other promises are cancelled.
   *
   * @param  {Promise} *[promises] The promises to use
   * @return {Promise} A new promise
   */
  exports.choose = function () {
    var promises, i, deferred;
    promises = Array.prototype.slice.call(arguments);
    function cancel() {
      var j;
      for (j = 0; j < promises.length; j += 1) {
        promises[j].cancel();
      }
    }
    deferred = new Deferred(cancel);
    function succeed(answer) {
      cancel();
      deferred.resolve(answer);
    }
    function failed(answer) {
      cancel();
      deferred.reject(answer);
    }
    function notify(j) {
      return function (answer) {
        deferred.notify({
          "promise": this,
          "index": j,
          "answer": answer
        });
      };
    }
    for (i = 0; i < promises.length; i += 1) {
      promises[i].then(succeed, failed, notify(i));
    }
    return deferred.promise;
  };

  /**
   * sleep(delay[, every]): Promise
   *
   * Resolve the promise after `timeout` milliseconds and notfies us every
   * `every` milliseconds.
   *
   *     Deferred.delay(50, 10).then(console.log, console.error, console.log);
   *     // // shows
   *     // 10 // from progress
   *     // 20 // from progress
   *     // 30 // from progress
   *     // 40 // from progress
   *     // 50 // from progress
   *     // 50 // from success
   *
   * @param  {Number} delay In milliseconds
   * @param  {Number} [every] In milliseconds
   * @return {Promise} A new promise
   */
  exports.sleep = function (delay, every) {
    var deferred, timeout, interval, now = 0;
    function cancel() {
      clearTimeout(timeout);
      clearInterval(interval);
    }
    deferred = new Deferred(cancel);
    if (typeof every === 'number' && isFinite(every)) {
      interval = setInterval(function () {
        now += every;
        deferred.notify(now);
      }, every);
    }
    timeout = setTimeout(function () {
      clearInterval(interval);
      deferred.notify(delay);
      deferred.resolve(delay);
    }, delay);
    return deferred.promise;
  };

  ////////////////////////////////////////////////////////////
  // http://wiki.commonjs.org/wiki/Promises/B
  // when(value, callback, errback_opt)

  /**
   * when(item, [onSuccess], [onError], [onProgress]): Promise
   *
   * Return an item as first parameter of the promise answer. If item is of
   * type Promise, the method will just return the promise. If item is of type
   * Deferred, the method will return the deferred promise.
   *
   *     Deferred.when('a').then(console.log); // shows 'a'
   *
   * @param  {Any} item The item to use
   * @param  {Function} [onSuccess] The callback called on success
   * @param  {Function} [onError] the callback called on error
   * @param  {Function} [onProgress] the callback called on progress
   * @return {Promise} The promise
   */
  exports.when = function (item, onSuccess, onError, onProgress) {
    if (typeof item === 'object' && item !== null) {
      if (typeof item.promise === 'object' && item.promise !== null &&
          typeof item.promise.then === 'function') {
        // item seams to be a Deferred
        return item.promise.then(onSuccess, onError, onProgress);
      }
      if (typeof item.then === 'function') {
        // item seams to be a Promise
        return item.then(onSuccess, onError, onProgress);
      }
    }
    // item is just a value, convert into fulfilled promise
    var deferred = new Deferred(), promise;
    if (typeof onSuccess === 'function') {
      promise = deferred.promise.then(onSuccess);
    } else {
      promise = deferred.promise;
    }
    deferred.resolve(item);
    return promise;
  };

  ////////////////////////////////////////////////////////////
  // http://wiki.commonjs.org/wiki/Promises/B
  // get(object, name)

  /**
   * get(dict, property): Promise
   *
   * Return the dict property as first parameter of the promise answer.
   *
   *     Deferred.get({'a': 'b'}, 'a').then(console.log); // shows 'b'
   *
   * @param  {Object} dict The object to use
   * @param  {String} property The object property name
   * @return {Promise} The promise
   */
  exports.get = function (dict, property) {
    var p = new Deferred();
    try {
      p.resolve(dict[property]);
    } catch (e) {
      p.reject(e);
    }
    return p;
  };

  ////////////////////////////////////////////////////////////
  // http://wiki.commonjs.org/wiki/Promises/B
  // put(object, name, value)

  /**
   * put(dict, property, value): Promise
   *
   * Set and return the dict property as first parameter of the promise answer.
   *
   *     Deferred.put({'a': 'b'}, 'a', 'c').then(console.log); // shows 'c'
   *
   * @param  {Object} dict The object to use
   * @param  {String} property The object property name
   * @param  {Any} value The value
   * @return {Promise} The promise
   */
  exports.put = function (dict, property, value) {
    var p = new Deferred();
    try {
      dict[property] = value;
      p.resolve(dict[property]);
    } catch (e) {
      p.reject(e);
    }
    return p;
  };

}));