/* 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/. */

var loop = loop || {};

/**
 * Monkeypatch getUserMedia in a way that prevents additional camera and
 * microphone prompts, at the cost of ignoring all constraints other than
 * the first set passed in.
 *
 * The first call to navigator.getUserMedia (also now aliased to
 * multiplexGum.getPermsAndCacheMedia to allow for explicit calling code)
 * will cause the underlying gUM implementation to be called.
 *
 * While permission is pending, subsequent calls will result in the callbacks
 * being queued. Once the call succeeds or fails, all queued success or
 * failure callbacks will be invoked.  Subsequent calls to either function will
 * cause the success or failure callback to be invoked immediately.
 */
loop.standaloneMedia = (function() {
  "use strict";

  function patchSymbolIfExtant(objectName, propertyName, replacement) {
    var object;
    if (window[objectName]) {
      object = window[objectName];
    }
    if (object && object[propertyName]) {
      object[propertyName] = replacement;
    }
  }

  // originalGum _must_ be on navigator; otherwise things blow up.
  // For TBPlugin users, navigator.originalGum is set after the TB SDK is loaded.
  navigator.originalGum = navigator.getUserMedia ||
                          navigator.mozGetUserMedia ||
                          navigator.webkitGetUserMedia;

  function _MultiplexGum() {
    this.reset();
  }

  _MultiplexGum.prototype = {
    /**
     * @see The docs at the top of this file for overall semantics,
     * & http://developer.mozilla.org/en-US/docs/NavigatorUserMedia.getUserMedia
     * for params, since this is intended to be purely a passthrough to gUM.
     */
    getPermsAndCacheMedia: function(constraints, onSuccess, onError) {
      function handleResult(callbacks, param) {
        // Operate on a copy of the array in case any of the callbacks
        // calls reset, which would cause an infinite-recursion.
        this.userMedia.successCallbacks = [];
        this.userMedia.errorCallbacks = [];
        callbacks.forEach(function(cb) {
          if (typeof cb == "function") {
            cb(param);
          }
        });
      }
      function handleSuccess(localStream) {
        this.userMedia.pending = false;
        this.userMedia.localStream = localStream;
        this.userMedia.error = null;
        handleResult.call(this, this.userMedia.successCallbacks.slice(0), localStream);
      }

      function handleError(error) {
        this.userMedia.pending = false;
        this.userMedia.error = error;
        handleResult.call(this, this.userMedia.errorCallbacks.slice(0), error);
        this.error = null;
      }

      if (this.userMedia.localStream &&
          this.userMedia.localStream.ended) {
        this.userMedia.localStream = null;
      }

      this.userMedia.errorCallbacks.push(onError);
      this.userMedia.successCallbacks.push(onSuccess);

      if (this.userMedia.localStream) {
        handleSuccess.call(this, this.userMedia.localStream);
        return;
      } else if (this.userMedia.error) {
        handleError.call(this, this.userMedia.error);
        return;
      }

      if (this.userMedia.pending) {
        return;
      }
      this.userMedia.pending = true;

      navigator.originalGum(constraints, handleSuccess.bind(this),
        handleError.bind(this));
    },

    /**
     * Reset the cached permissions, callbacks, and media to their default
     * state and call any error callbacks to let any waiting callers know
     * not to ever expect any more callbacks.  We use "PERMISSION_DENIED",
     * for lack of a better, more specific gUM code that callers are likely
     * to be prepared to handle.
     */
    reset: function() {
      // When called from the ctor, userMedia is not created yet.
      if (this.userMedia) {
        this.userMedia.errorCallbacks.forEach(function(cb) {
          if (typeof cb == "function") {
            cb("PERMISSION_DENIED");
          }
        });
        if (this.userMedia.localStream &&
            typeof this.userMedia.localStream.stop == "function") {
          this.userMedia.localStream.stop();
        }
      }
      this.userMedia = {
        error: null,
        localStream: null,
        pending: false,
        errorCallbacks: [],
        successCallbacks: []
      };
    }
  };

  var singletonMultiplexGum = new _MultiplexGum();
  function myGetUserMedia() {
    // This function is needed to pull in the instance
    // of the singleton for tests to overwrite the used instance.
    singletonMultiplexGum.getPermsAndCacheMedia.apply(singletonMultiplexGum, arguments);
  }
  patchSymbolIfExtant("navigator", "mozGetUserMedia", myGetUserMedia);
  patchSymbolIfExtant("navigator", "webkitGetUserMedia", myGetUserMedia);
  patchSymbolIfExtant("navigator", "getUserMedia", myGetUserMedia);
  patchSymbolIfExtant("TBPlugin", "getUserMedia", myGetUserMedia);

  return {
    multiplexGum: singletonMultiplexGum,
    _MultiplexGum: _MultiplexGum,
    setSingleton: function(singleton) {
      singletonMultiplexGum = singleton;
    }
  };
})();
