// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview This implementes a future promise class.
 */

cr.define('cr', function() {

  /**
   * Sentinel used to mark a value as pending.
   */
  const PENDING_VALUE = {};

  /**
   * Creates a future promise.
   * @param {*=} opt_value The value to set the promise to. If set completes
   *     the promise immediately.
   * @constructor
   */
  function Promise(opt_value) {
    /**
     * An array of the callbacks.
     * @type {!Array.<!Function>}
     * @private
     */
    this.callbacks_ = [];

    if (arguments.length > 0)
      this.value = opt_value;
  }

  Promise.prototype = {
    /**
     * The current value.
     * @type {*}
     * @private
     */
    value_: PENDING_VALUE,

    /**
     * The value of the future promise. Accessing this before the promise has
     * been fulfilled will throw an error. If this is set to an exception
     * accessing this will throw as well.
     * @type {*}
     */
    get value() {
      return this.done ? this.value_ : undefined;
    },
    set value(value) {
      if (!this.done) {
        this.value_ = value;
        for (var i = 0; i < this.callbacks_.length; i++) {
          this.callbacks_[i].call(null, value);
        }
        this.callbacks_.length = 0;
      }
    },

    /**
     * Whether the future promise has been fulfilled.
     * @type {boolean}
     */
    get done() {
      return this.value_ !== PENDING_VALUE;
    },

    /**
     * Adds a listener to the future promise. The function will be called when
     * the promise is fulfilled. If the promise is already fullfilled this will
     * never call the function.
     * @param {!Function} fun The function to call.
     */
    addListener: function(fun) {
      if (this.done)
        fun(this.value);
      else
        this.callbacks_.push(fun);
    },

    /**
     * Removes a previously added listener from the future promise.
     * @param {!Function} fun The function to remove.
     */
    removeListener: function(fun) {
      var i = this.callbacks_.indexOf(fun);
      if (i >= 0)
        this.callbacks_.splice(i, 1);
    },

    /**
     * If the promise is done then this returns the string representation of
     * the value.
     * @return {string} The string representation of the promise.
     * @override
     */
    toString: function() {
      if (this.done)
        return String(this.value);
      else
        return '[object Promise]';
    },

    /**
     * Override to allow arithmetic.
     * @override
     */
    valueOf: function() {
      return this.value;
    }
  };

  /**
   * When a future promise is done call {@code fun}. This also calls the
   * function if the promise has already been fulfilled.
   * @param {!Promise} p The promise.
   * @param {!Function} fun The function to call when the promise is fulfilled.
   */
  Promise.when = function(p, fun) {
    p.addListener(fun);
  };

  /**
   * Creates a new promise the will be fulfilled after {@code t} ms.
   * @param {number} t The time to wait before the promise is fulfilled.
   * @param {*=} opt_value The value to return after the wait.
   * @return {!Promise} The new future promise.
   */
  Promise.wait = function(t, opt_value) {
    var p = new Promise;
    window.setTimeout(function() {
      p.value = opt_value;
    }, t);
    return p;
  };

  /**
   * Creates a new future promise that is fulfilled when any of the promises are
   * fulfilled. The value of the returned promise will be the value of the first
   * fulfilled promise.
   * @param {...!Promise} var_args The promises used to build up the new
   *     promise.
   * @return {!Promise} The new promise that will be fulfilled when any of the
   *     passed in promises are fulfilled.
   */
  Promise.any = function(var_args) {
    var p = new Promise;
    function f(v) {
      p.value = v;
    }
    for (var i = 0; i < arguments.length; i++) {
      arguments[i].addListener(f);
    }
    return p;
  };

  /**
   * Creates a new future promise that is fulfilled when all of the promises are
   * fulfilled. The value of the returned promise is an array of the values of
   * the promises passed in.
   * @param {...!Promise} var_args The promises used to build up the new
   *     promise.
   * @return {!Promise} The promise that wraps all the promises in the array.
   */
  Promise.all = function(var_args) {
    var p = new Promise;
    var args = Array.prototype.slice.call(arguments);
    var count = args.length;
    if (!count) {
      p.value = [];
      return p;
    }

    function f(v) {
      count--;
      if (!count) {
        p.value = args.map(function(argP) {
          return argP.value;
        });
      }
    }

    // Do not use count here since count may be decremented in the call to
    // addListener if the promise is already done.
    for (var i = 0; i < args.length; i++) {
      args[i].addListener(f);
    }

    return p;
  };

  /**
   * Wraps an event in a future promise.
   * @param {!EventTarget} target The object that dispatches the event.
   * @param {string} type The type of the event.
   * @param {boolean=} opt_useCapture Whether to listen to the capture phase or
   *     the bubble phase.
   * @return {!Promise} The promise that will be fulfilled when the event is
   *     dispatched.
   */
  Promise.event = function(target, type, opt_useCapture) {
    var p = new Promise;
    target.addEventListener(type, function(e) {
      p.value = e;
    }, opt_useCapture);
    return p;
  };

  return {
    Promise: Promise
  };
});