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

"use strict";

const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;

this.EXPORTED_SYMBOLS = [ "ContentWebRTC" ];

Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
                                   "@mozilla.org/mediaManagerService;1",
                                   "nsIMediaManagerService");

const kBrowserURL = "chrome://browser/content/browser.xul";

this.ContentWebRTC = {
  _initialized: false,

  init: function() {
    if (this._initialized)
      return;

    this._initialized = true;
    Services.obs.addObserver(handleGUMRequest, "getUserMedia:request", false);
    Services.obs.addObserver(handlePCRequest, "PeerConnection:request", false);
    Services.obs.addObserver(updateIndicators, "recording-device-events", false);
    Services.obs.addObserver(removeBrowserSpecificIndicator, "recording-window-ended", false);

    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT)
      Services.obs.addObserver(processShutdown, "content-child-shutdown", false);
  },

  uninit: function() {
    Services.obs.removeObserver(handleGUMRequest, "getUserMedia:request");
    Services.obs.removeObserver(handlePCRequest, "PeerConnection:request");
    Services.obs.removeObserver(updateIndicators, "recording-device-events");
    Services.obs.removeObserver(removeBrowserSpecificIndicator, "recording-window-ended");

    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT)
      Services.obs.removeObserver(processShutdown, "content-child-shutdown");

    this._initialized = false;
  },

  // Called only for 'unload' to remove pending gUM prompts in reloaded frames.
  handleEvent: function(aEvent) {
    let contentWindow = aEvent.target.defaultView;
    let mm = getMessageManagerForWindow(contentWindow);
    for (let key of contentWindow.pendingGetUserMediaRequests.keys()) {
      mm.sendAsyncMessage("webrtc:CancelRequest", key);
    }
    for (let key of contentWindow.pendingPeerConnectionRequests.keys()) {
      mm.sendAsyncMessage("rtcpeer:CancelRequest", key);
    }
  },

  receiveMessage: function(aMessage) {
    switch (aMessage.name) {
      case "rtcpeer:Allow":
      case "rtcpeer:Deny": {
        let callID = aMessage.data.callID;
        let contentWindow = Services.wm.getOuterWindowWithId(aMessage.data.windowID);
        forgetPCRequest(contentWindow, callID);
        let topic = (aMessage.name == "rtcpeer:Allow") ? "PeerConnection:response:allow" :
                                                         "PeerConnection:response:deny";
        Services.obs.notifyObservers(null, topic, callID);
        break;
      }
      case "webrtc:Allow": {
        let callID = aMessage.data.callID;
        let contentWindow = Services.wm.getOuterWindowWithId(aMessage.data.windowID);
        let devices = contentWindow.pendingGetUserMediaRequests.get(callID);
        forgetGUMRequest(contentWindow, callID);

        let allowedDevices = Cc["@mozilla.org/array;1"]
                               .createInstance(Ci.nsIMutableArray);
        for (let deviceIndex of aMessage.data.devices)
           allowedDevices.appendElement(devices[deviceIndex], /* weak =*/ false);

        Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", callID);
        break;
      }
      case "webrtc:Deny":
        denyGUMRequest(aMessage.data);
        break;
      case "webrtc:StopSharing":
        Services.obs.notifyObservers(null, "getUserMedia:revoke", aMessage.data);
        break;
    }
  }
};

function handlePCRequest(aSubject, aTopic, aData) {
  let { windowID, innerWindowID, callID, isSecure } = aSubject;
  let contentWindow = Services.wm.getOuterWindowWithId(windowID);

  let mm = getMessageManagerForWindow(contentWindow);
  if (!mm) {
    // Workaround for Bug 1207784. To use WebRTC, add-ons right now use
    // hiddenWindow.mozRTCPeerConnection which is only privileged on OSX. Other
    // platforms end up here without a message manager.
    // TODO: Remove once there's a better way (1215591).

    // Skip permission check in the absence of a message manager.
    Services.obs.notifyObservers(null, "PeerConnection:response:allow", callID);
    return;
  }

  if (!contentWindow.pendingPeerConnectionRequests) {
    setupPendingListsInitially(contentWindow);
  }
  contentWindow.pendingPeerConnectionRequests.add(callID);

  let request = {
    windowID: windowID,
    innerWindowID: innerWindowID,
    callID: callID,
    documentURI: contentWindow.document.documentURI,
    secure: isSecure,
  };
  mm.sendAsyncMessage("rtcpeer:Request", request);
}

function handleGUMRequest(aSubject, aTopic, aData) {
  let constraints = aSubject.getConstraints();
  let secure = aSubject.isSecure;
  let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);

  contentWindow.navigator.mozGetUserMediaDevices(
    constraints,
    function (devices) {
      // If the window has been closed while we were waiting for the list of
      // devices, there's nothing to do in the callback anymore.
      if (contentWindow.closed)
        return;

      prompt(contentWindow, aSubject.windowID, aSubject.callID,
             constraints, devices, secure);
    },
    function (error) {
      // bug 827146 -- In the future, the UI should catch NotFoundError
      // and allow the user to plug in a device, instead of immediately failing.
      denyGUMRequest({callID: aSubject.callID}, error);
    },
    aSubject.innerWindowID,
    aSubject.callID);
}

function prompt(aContentWindow, aWindowID, aCallID, aConstraints, aDevices, aSecure) {
  let audioDevices = [];
  let videoDevices = [];
  let devices = [];

  // MediaStreamConstraints defines video as 'boolean or MediaTrackConstraints'.
  let video = aConstraints.video || aConstraints.picture;
  let audio = aConstraints.audio;
  let sharingScreen = video && typeof(video) != "boolean" &&
                      video.mediaSource != "camera";
  let sharingAudio = audio && typeof(audio) != "boolean" &&
                     audio.mediaSource != "microphone";
  for (let device of aDevices) {
    device = device.QueryInterface(Ci.nsIMediaDevice);
    switch (device.type) {
      case "audio":
        // Check that if we got a microphone, we have not requested an audio
        // capture, and if we have requested an audio capture, we are not
        // getting a microphone instead.
        if (audio && (device.mediaSource == "microphone") != sharingAudio) {
          audioDevices.push({name: device.name, deviceIndex: devices.length,
                             id: device.rawId, mediaSource: device.mediaSource});
          devices.push(device);
        }
        break;
      case "video":
        // Verify that if we got a camera, we haven't requested a screen share,
        // or that if we requested a screen share we aren't getting a camera.
        if (video && (device.mediaSource == "camera") != sharingScreen) {
          let deviceObject = {name: device.name, deviceIndex: devices.length,
                              id: device.rawId, mediaSource: device.mediaSource};
          if (device.scary)
            deviceObject.scary = true;
          videoDevices.push(deviceObject);
          devices.push(device);
        }
        break;
    }
  }

  let requestTypes = [];
  if (videoDevices.length)
    requestTypes.push(sharingScreen ? "Screen" : "Camera");
  if (audioDevices.length)
    requestTypes.push(sharingAudio ? "AudioCapture" : "Microphone");

  if (!requestTypes.length) {
    denyGUMRequest({callID: aCallID}, "NotFoundError");
    return;
  }

  if (!aContentWindow.pendingGetUserMediaRequests) {
    setupPendingListsInitially(aContentWindow);
  }
  aContentWindow.pendingGetUserMediaRequests.set(aCallID, devices);

  let request = {
    callID: aCallID,
    windowID: aWindowID,
    documentURI: aContentWindow.document.documentURI,
    secure: aSecure,
    requestTypes: requestTypes,
    sharingScreen: sharingScreen,
    sharingAudio: sharingAudio,
    audioDevices: audioDevices,
    videoDevices: videoDevices
  };

  let mm = getMessageManagerForWindow(aContentWindow);
  mm.sendAsyncMessage("webrtc:Request", request);
}

function denyGUMRequest(aData, aError) {
  let msg = null;
  if (aError) {
    msg = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
    msg.data = aError;
  }
  Services.obs.notifyObservers(msg, "getUserMedia:response:deny", aData.callID);

  if (!aData.windowID)
    return;
  let contentWindow = Services.wm.getOuterWindowWithId(aData.windowID);
  if (contentWindow.pendingGetUserMediaRequests)
    forgetGUMRequest(contentWindow, aData.callID);
}

function forgetGUMRequest(aContentWindow, aCallID) {
  aContentWindow.pendingGetUserMediaRequests.delete(aCallID);
  forgetPendingListsEventually(aContentWindow);
}

function forgetPCRequest(aContentWindow, aCallID) {
  aContentWindow.pendingPeerConnectionRequests.delete(aCallID);
  forgetPendingListsEventually(aContentWindow);
}

function setupPendingListsInitially(aContentWindow) {
  if (aContentWindow.pendingGetUserMediaRequests) {
    return;
  }
  aContentWindow.pendingGetUserMediaRequests = new Map();
  aContentWindow.pendingPeerConnectionRequests = new Set();
  aContentWindow.addEventListener("unload", ContentWebRTC);
}

function forgetPendingListsEventually(aContentWindow) {
  if (aContentWindow.pendingGetUserMediaRequests.size ||
      aContentWindow.pendingPeerConnectionRequests.size) {
    return;
  }
  aContentWindow.pendingGetUserMediaRequests = null;
  aContentWindow.pendingPeerConnectionRequests = null;
  aContentWindow.removeEventListener("unload", ContentWebRTC);
}

function updateIndicators(aSubject, aTopic, aData) {
  if (aSubject instanceof Ci.nsIPropertyBag &&
      aSubject.getProperty("requestURL") == kBrowserURL) {
    // Ignore notifications caused by the browser UI showing previews.
    return;
  }

  let contentWindowArray = MediaManagerService.activeMediaCaptureWindows;
  let count = contentWindowArray.length;

  let state = {
    showGlobalIndicator: count > 0,
    showCameraIndicator: false,
    showMicrophoneIndicator: false,
    showScreenSharingIndicator: ""
  };

  let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
               .getService(Ci.nsIMessageSender);
  cpmm.sendAsyncMessage("webrtc:UpdatingIndicators");

  // If several iframes in the same page use media streams, it's possible to
  // have the same top level window several times. We use a Set to avoid
  // sending duplicate notifications.
  let contentWindows = new Set();
  for (let i = 0; i < count; ++i) {
    contentWindows.add(contentWindowArray.queryElementAt(i, Ci.nsISupports).top);
  }

  for (let contentWindow of contentWindows) {
    if (contentWindow.document.documentURI == kBrowserURL) {
      // There may be a preview shown at the same time as other streams.
      continue;
    }

    let tabState = getTabStateForContentWindow(contentWindow);
    if (tabState.camera)
      state.showCameraIndicator = true;
    if (tabState.microphone)
      state.showMicrophoneIndicator = true;
    if (tabState.screen) {
      if (tabState.screen == "Screen") {
        state.showScreenSharingIndicator = "Screen";
      }
      else if (tabState.screen == "Window") {
        if (state.showScreenSharingIndicator != "Screen")
          state.showScreenSharingIndicator = "Window";
      }
      else if (tabState.screen == "Application") {
        if (!state.showScreenSharingIndicator)
          state.showScreenSharingIndicator = "Application";
      }
      else if (tabState.screen == "Browser") {
        if (!state.showScreenSharingIndicator)
          state.showScreenSharingIndicator = "Browser";
      }
    }
    let mm = getMessageManagerForWindow(contentWindow);
    mm.sendAsyncMessage("webrtc:UpdateBrowserIndicators", tabState);
  }

  cpmm.sendAsyncMessage("webrtc:UpdateGlobalIndicators", state);
}

function removeBrowserSpecificIndicator(aSubject, aTopic, aData) {
  let contentWindow = Services.wm.getOuterWindowWithId(aData).top;
  if (contentWindow.document.documentURI == kBrowserURL) {
    // Ignore notifications caused by the browser UI showing previews.
    return;
  }

  let tabState = getTabStateForContentWindow(contentWindow);
  if (!tabState.camera && !tabState.microphone && !tabState.screen)
    tabState = {windowId: tabState.windowId};

  let mm = getMessageManagerForWindow(contentWindow);
  if (mm)
    mm.sendAsyncMessage("webrtc:UpdateBrowserIndicators", tabState);
}

function getTabStateForContentWindow(aContentWindow) {
  let camera = {}, microphone = {}, screen = {}, window = {}, app = {}, browser = {};
  MediaManagerService.mediaCaptureWindowState(aContentWindow, camera, microphone,
                                              screen, window, app, browser);
  let tabState = {camera: camera.value, microphone: microphone.value};
  if (screen.value)
    tabState.screen = "Screen";
  else if (window.value)
    tabState.screen = "Window";
  else if (app.value)
    tabState.screen = "Application";
  else if (browser.value)
    tabState.screen = "Browser";

  tabState.windowId = getInnerWindowIDForWindow(aContentWindow);
  tabState.documentURI = aContentWindow.document.documentURI;

  return tabState;
}

function getInnerWindowIDForWindow(aContentWindow) {
  return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                       .getInterface(Ci.nsIDOMWindowUtils)
                       .currentInnerWindowID;
}

function getMessageManagerForWindow(aContentWindow) {
  let ir = aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                         .getInterface(Ci.nsIDocShell)
                         .sameTypeRootTreeItem
                         .QueryInterface(Ci.nsIInterfaceRequestor);
  try {
    // If e10s is disabled, this throws NS_NOINTERFACE for closed tabs.
    return ir.getInterface(Ci.nsIContentFrameMessageManager);
  } catch (e) {
    if (e.result == Cr.NS_NOINTERFACE) {
      return null;
    }
    throw e;
  }
}

function processShutdown() {
  ContentWebRTC.uninit();
}
