/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* 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/. */

/* This content script should work in any browser or iframe and should not
 * depend on the frame being contained in tabbrowser. */

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

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");

// Tie `content` to this frame scripts' global scope explicitly. If we don't, then
// `content` might be out of eval's scope and GC'ed before this script is done.
// See bug 1229195 for empirical proof.
var gContent = content;

// social frames are always treated as app tabs
docShell.isAppTab = true;

var gDOMContentLoaded = false;
addEventListener("DOMContentLoaded", function() {
  gDOMContentLoaded = true;
  sendAsyncMessage("DOMContentLoaded");
});
var gDOMTitleChangedByUs = false;
addEventListener("DOMTitleChanged", function(e) {
  if (!gDOMTitleChangedByUs) {
    sendAsyncMessage("Social:DOMTitleChanged", {
      title: e.target.title
    });
  }
  gDOMTitleChangedByUs = false;
});
var gHookedWindowCloseForPanelClose = false;

// Error handling class used to listen for network errors in the social frames
// and replace them with a social-specific error page
SocialErrorListener = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMEventListener,
                                         Ci.nsIWebProgressListener,
                                         Ci.nsISupportsWeakReference,
                                         Ci.nsISupports]),

  defaultTemplate: "about:socialerror?mode=tryAgainOnly&url=%{url}&origin=%{origin}",
  urlTemplate: null,

  init() {
    addMessageListener("Loop:MonitorPeerConnectionLifecycle", this);
    addMessageListener("Loop:GetAllWebrtcStats", this);
    addMessageListener("Social:CustomEvent", this);
    addMessageListener("Social:EnsureFocus", this);
    addMessageListener("Social:EnsureFocusElement", this);
    addMessageListener("Social:HookWindowCloseForPanelClose", this);
    addMessageListener("Social:ListenForEvents", this);
    addMessageListener("Social:SetDocumentTitle", this);
    addMessageListener("Social:SetErrorURL", this);
    addMessageListener("Social:WaitForDocumentVisible", this);
    addMessageListener("WaitForDOMContentLoaded", this);
    let webProgress = docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                              .getInterface(Components.interfaces.nsIWebProgress);
    webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_REQUEST |
                                          Ci.nsIWebProgress.NOTIFY_LOCATION);
  },

  receiveMessage(message) {
    let document = content.document;

    switch (message.name) {
      case "Loop:GetAllWebrtcStats":
        content.WebrtcGlobalInformation.getAllStats(allStats => {
          content.WebrtcGlobalInformation.getLogging("", logs => {
            sendAsyncMessage("Loop:GetAllWebrtcStats", {
              allStats: allStats,
              logs: logs
            });
          });
        }, message.data.peerConnectionID);
        break;
      case "Loop:MonitorPeerConnectionLifecycle":
        let ourID = content.QueryInterface(Ci.nsIInterfaceRequestor)
          .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;

        let onPCLifecycleChange = (pc, winID, type) => {
          if (winID != ourID) {
            return;
          }

          sendAsyncMessage("Loop:PeerConnectionLifecycleChange", {
            iceConnectionState: pc.iceConnectionState,
            locationHash: content.location.hash,
            peerConnectionID: pc.id,
            type: type
          });
        };

        let pc_static = new content.RTCPeerConnectionStatic();
        pc_static.registerPeerConnectionLifecycleCallback(onPCLifecycleChange);
        break;
      case "Social:CustomEvent":
        let ev = new content.CustomEvent(message.data.name, message.data.detail ?
          { detail: message.data.detail } : null);
        content.dispatchEvent(ev);
        break;
      case "Social:EnsureFocus":
        Services.focus.focusedWindow = content;
        sendAsyncMessage("Social:FocusEnsured");
        break;
      case "Social:EnsureFocusElement":
        let fm = Services.focus;
        fm.moveFocus(document.defaultView, null, fm.MOVEFOCUS_FIRST, fm.FLAG_NOSCROLL);
        sendAsyncMessage("Social:FocusEnsured");
        break;
      case "Social:HookWindowCloseForPanelClose":
        if (gHookedWindowCloseForPanelClose) {
          break;
        }
        gHookedWindowCloseForPanelClose = true;
        // We allow window.close() to close the panel, so add an event handler for
        // this, then cancel the event (so the window itself doesn't die) and
        // close the panel instead.
        // However, this is typically affected by the dom.allow_scripts_to_close_windows
        // preference, but we can avoid that check by setting a flag on the window.
        let dwu = content.QueryInterface(Ci.nsIInterfaceRequestor)
           .getInterface(Ci.nsIDOMWindowUtils);
        dwu.allowScriptsToClose();

        content.addEventListener("DOMWindowClose", function _mozSocialDOMWindowClose(evt) {
          // preventDefault stops the default window.close() function being called,
          // which doesn't actually close anything but causes things to get into
          // a bad state (an internal 'closed' flag is set and debug builds start
          // asserting as the window is used.).
          // None of the windows we inject this API into are suitable for this
          // default close behaviour, so even if we took no action above, we avoid
          // the default close from doing anything.
          evt.preventDefault();

          sendAsyncMessage("Social:DOMWindowClose");
        }, true);
        break;
      case "Social:ListenForEvents":
        for (let eventName of message.data.eventNames) {
          content.addEventListener(eventName, this);
        }
        break;
      case "Social:SetDocumentTitle":
        let title = message.data.title;
        if (title && (title = title.trim())) {
          gDOMTitleChangedByUs = true;
          document.title = title;
        }
        break;
      case "Social:SetErrorURL":
        // Either a url or null to reset to default template.
        this.urlTemplate = message.data.template;
        break;
      case "Social:WaitForDocumentVisible":
        if (!document.hidden) {
          sendAsyncMessage("Social:DocumentVisible");
          break;
        }

        document.addEventListener("visibilitychange", function onVisibilityChanged() {
          document.removeEventListener("visibilitychange", onVisibilityChanged);
          sendAsyncMessage("Social:DocumentVisible");
        });
        break;
      case "WaitForDOMContentLoaded":
        if (gDOMContentLoaded) {
          sendAsyncMessage("DOMContentLoaded");
        }
        break;
    }
  },

  handleEvent: function(event) {
    sendAsyncMessage("Social:CustomEvent", {
      name: event.type
    });
  },

  setErrorPage() {
    // if this is about:providerdirectory, use the directory iframe
    let frame = docShell.chromeEventHandler;
    let origin = frame.getAttribute("origin");
    let src = frame.getAttribute("src");
    if (src == "about:providerdirectory") {
      frame = content.document.getElementById("activation-frame");
      src = frame.getAttribute("src");
    }

    let url = this.urlTemplate || this.defaultTemplate;
    url = url.replace("%{url}", encodeURIComponent(src));
    url = url.replace("%{origin}", encodeURIComponent(origin));
    if (frame != docShell.chromeEventHandler) {
      // Unable to access frame.docShell here. This is our own frame and doesn't
      // provide reload, so we'll just set the src.
      frame.setAttribute("src", url);
    } else {
      let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
      webNav.loadURI(url, null, null, null, null);
    }
    sendAsyncMessage("Social:ErrorPageNotify", {
        origin: origin,
        url: src
    });
  },

  onStateChange(aWebProgress, aRequest, aState, aStatus) {
    let failure = false;
    if ((aState & Ci.nsIWebProgressListener.STATE_STOP)) {
      if (aRequest instanceof Ci.nsIHttpChannel) {
        try {
          // Change the frame to an error page on 4xx (client errors)
          // and 5xx (server errors).  responseStatus throws if it is not set.
          failure = aRequest.responseStatus >= 400 &&
                    aRequest.responseStatus < 600;
        } catch (e) {
          failure = aStatus != Components.results.NS_OK;
        }
      }
    }

    // Calling cancel() will raise some OnStateChange notifications by itself,
    // so avoid doing that more than once
    if (failure && aStatus != Components.results.NS_BINDING_ABORTED) {
      // if tp is enabled and we get a failure, ignore failures (ie. STATE_STOP)
      // on child resources since they *may* have been blocked. We don't have an
      // easy way to know if a particular url is blocked by TP, only that
      // something was.
      if (docShell.hasTrackingContentBlocked) {
        let frame = docShell.chromeEventHandler;
        let src = frame.getAttribute("src");
        if (aRequest && aRequest.name != src) {
          Cu.reportError("SocialErrorListener ignoring blocked content error for " + aRequest.name);
          return;
        }
      }

      aRequest.cancel(Components.results.NS_BINDING_ABORTED);
      this.setErrorPage();
    }
  },

  onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
    if (aRequest && aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
      aRequest.cancel(Components.results.NS_BINDING_ABORTED);
      this.setErrorPage();
    }
  },

  onProgressChange() {},
  onStatusChange() {},
  onSecurityChange() {},
};

SocialErrorListener.init();
