/**
 * setImmediate polyfill v1.0.1, supports IE9+
 * © 2014–2015 Dmitry Korobkin
 * Released under the MIT license
 * github.com/Octane/setImmediate
 */
window.setImmediate || (function () {
  let uid = 0;
  const storage = {};
  let firstCall = true;
  const { slice } = Array.prototype;
  const message = 'setImmediatePolyfillMessage';

  function fastApply(args) {
    const func = args[0];
    switch (args.length) {
      case 1:
        return func();
      case 2:
        return func(args[1]);
      case 3:
        return func(args[1], args[2]);
    }
    return func.apply(window, slice.call(args, 1));
  }

  function callback(event) {
    const key = event.data;
    let data;
    if (typeof key === 'string' && key.indexOf(message) == 0) {
      data = storage[key];
      if (data) {
        delete storage[key];
        fastApply(data);
      }
    }
  }

  window.setImmediate = function setImmediate() {
    const id = uid++;
    const key = message + id;
    let i = arguments.length;
    const args = new Array(i);
    while (i--) {
      args[i] = arguments[i];
    }
    storage[key] = args;
    if (firstCall) {
      firstCall = false;
      window.addEventListener('message', callback);
    }
    window.postMessage(key, '*');
    return id;
  };

  window.clearImmediate = function clearImmediate(id) {
    delete storage[message + id];
  };
}());

/**
 * Promise polyfill v1.0.10
 * requires setImmediate
 *
 * © 2014–2015 Dmitry Korobkin
 * Released under the MIT license
 * github.com/Octane/Promise
 */
(function (global) {
  const STATUS = '[[PromiseStatus]]';
  const VALUE = '[[PromiseValue]]';
  const ON_FUlFILLED = '[[OnFulfilled]]';
  const ON_REJECTED = '[[OnRejected]]';
  const ORIGINAL_ERROR = '[[OriginalError]]';
  const PENDING = 'pending';
  const INTERNAL_PENDING = 'internal pending';
  const FULFILLED = 'fulfilled';
  const REJECTED = 'rejected';
  const NOT_ARRAY = 'not an array.';
  const REQUIRES_NEW = 'constructor Promise requires "new".';
  const CHAINING_CYCLE = 'then() cannot return same Promise that it resolves.';

  const setImmediate = global.setImmediate || require('timers').setImmediate;
  const isArray = Array.isArray || function (anything) {
    return Object.prototype.toString.call(anything) == '[object Array]';
  };

  function InternalError(originalError) {
    this[ORIGINAL_ERROR] = originalError;
  }

  function isInternalError(anything) {
    return anything instanceof InternalError;
  }

  function isObject(anything) {
    // Object.create(null) instanceof Object → false
    return Object(anything) === anything;
  }

  function isCallable(anything) {
    return typeof anything === 'function';
  }

  function isPromise(anything) {
    return anything instanceof Promise;
  }

  function identity(value) {
    return value;
  }

  function thrower(reason) {
    throw reason;
  }

  function enqueue(promise, onFulfilled, onRejected) {
    if (!promise[ON_FUlFILLED]) {
      promise[ON_FUlFILLED] = [];
      promise[ON_REJECTED] = [];
    }
    promise[ON_FUlFILLED].push(onFulfilled);
    promise[ON_REJECTED].push(onRejected);
  }

  function clearAllQueues(promise) {
    delete promise[ON_FUlFILLED];
    delete promise[ON_REJECTED];
  }

  function callEach(queue) {
    let i;
    const { length } = queue;
    for (i = 0; i < length; i++) {
      queue[i]();
    }
  }

  function call(resolve, reject, value) {
    const anything = toPromise(value);
    if (isPromise(anything)) {
      anything.then(resolve, reject);
    } else if (isInternalError(anything)) {
      reject(anything[ORIGINAL_ERROR]);
    } else {
      resolve(value);
    }
  }

  function toPromise(anything) {
    let then;
    if (isPromise(anything)) {
      return anything;
    }
    if (isObject(anything)) {
      try {
        then = anything.then;
      } catch (error) {
        return new InternalError(error);
      }
      if (isCallable(then)) {
        return new Promise((resolve, reject) => {
          setImmediate(() => {
            try {
              then.call(anything, resolve, reject);
            } catch (error) {
              reject(error);
            }
          });
        });
      }
    }
    return null;
  }

  function resolvePromise(promise, resolver) {
    function resolve(value) {
      if (promise[STATUS] == PENDING) {
        fulfillPromise(promise, value);
      }
    }
    function reject(reason) {
      if (promise[STATUS] == PENDING) {
        rejectPromise(promise, reason);
      }
    }
    try {
      resolver(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  function fulfillPromise(promise, value) {
    let queue;
    const anything = toPromise(value);
    if (isPromise(anything)) {
      promise[STATUS] = INTERNAL_PENDING;
      anything.then(
        (value) => {
          fulfillPromise(promise, value);
        },
        (reason) => {
          rejectPromise(promise, reason);
        },
      );
    } else if (isInternalError(anything)) {
      rejectPromise(promise, anything[ORIGINAL_ERROR]);
    } else {
      promise[STATUS] = FULFILLED;
      promise[VALUE] = value;
      queue = promise[ON_FUlFILLED];
      if (queue && queue.length) {
        clearAllQueues(promise);
        callEach(queue);
      }
    }
  }

  function rejectPromise(promise, reason) {
    const queue = promise[ON_REJECTED];
    promise[STATUS] = REJECTED;
    promise[VALUE] = reason;
    if (queue && queue.length) {
      clearAllQueues(promise);
      callEach(queue);
    }
  }

  function Promise(resolver) {
    const promise = this;
    if (!isPromise(promise)) {
      throw new TypeError(REQUIRES_NEW);
    }
    promise[STATUS] = PENDING;
    promise[VALUE] = undefined;
    resolvePromise(promise, resolver);
  }

  Promise.prototype.then = function (onFulfilled, onRejected) {
    const promise = this;
    let nextPromise;
    onFulfilled = isCallable(onFulfilled) ? onFulfilled : identity;
    onRejected = isCallable(onRejected) ? onRejected : thrower;
    nextPromise = new Promise((resolve, reject) => {
      function tryCall(func) {
        let value;
        try {
          value = func(promise[VALUE]);
        } catch (error) {
          reject(error);
          return;
        }
        if (value === nextPromise) {
          reject(new TypeError(CHAINING_CYCLE));
        } else {
          call(resolve, reject, value);
        }
      }
      function asyncOnFulfilled() {
        setImmediate(tryCall, onFulfilled);
      }
      function asyncOnRejected() {
        setImmediate(tryCall, onRejected);
      }
      switch (promise[STATUS]) {
        case FULFILLED:
          asyncOnFulfilled();
          break;
        case REJECTED:
          asyncOnRejected();
          break;
        default:
          enqueue(promise, asyncOnFulfilled, asyncOnRejected);
      }
    });
    return nextPromise;
  };

  Promise.prototype.catch = function (onRejected) {
    return this.then(identity, onRejected);
  };

  Promise.resolve = function (value) {
    const anything = toPromise(value);
    if (isPromise(anything)) {
      return anything;
    }
    return new Promise((resolve, reject) => {
      if (isInternalError(anything)) {
        reject(anything[ORIGINAL_ERROR]);
      } else {
        resolve(value);
      }
    });
  };

  Promise.reject = function (reason) {
    return new Promise((resolve, reject) => {
      reject(reason);
    });
  };

  Promise.race = function (values) {
    return new Promise((resolve, reject) => {
      let i;
      let length;
      if (isArray(values)) {
        length = values.length;
        for (i = 0; i < length; i++) {
          call(resolve, reject, values[i]);
        }
      } else {
        reject(new TypeError(NOT_ARRAY));
      }
    });
  };

  Promise.all = function (values) {
    return new Promise((resolve, reject) => {
      let fulfilledCount = 0;
      let promiseCount = 0;
      let anything;
      let length;
      let value;
      let i;
      if (isArray(values)) {
        values = values.slice(0);
        length = values.length;
        for (i = 0; i < length; i++) {
          value = values[i];
          anything = toPromise(value);
          if (isPromise(anything)) {
            promiseCount++;
            anything.then(
              (function (index) {
                return function (value) {
                  values[index] = value;
                  fulfilledCount++;
                  if (fulfilledCount == promiseCount) {
                    resolve(values);
                  }
                };
              }(i)),
              reject,
            );
          } else if (isInternalError(anything)) {
            reject(anything[ORIGINAL_ERROR]);
          } else {
            // [1, , 3] → [1, undefined, 3]
            values[i] = value;
          }
        }
        if (!promiseCount) {
          resolve(values);
        }
      } else {
        reject(new TypeError(NOT_ARRAY));
      }
    });
  };

  if (typeof module !== 'undefined' && module.exports) {
    module.exports = global.Promise || Promise;
  } else if (!global.Promise) {
    global.Promise = Promise;
  }
}(this));
