/* 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 Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;

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

function defineNoReturnMethod(methodName) {
  return function noReturnMethod() {
    let args = Array.slice(arguments);
    this._sendToParent(methodName, args);
  };
}

function defineDOMRequestMethod(methodName) {
  return function domRequestMethod() {
    let args = Array.slice(arguments);
    return this._sendDOMRequest(methodName, args);
  };
}

function defineUnimplementedMethod(methodName) {
  return function unimplementedMethod() {
    throw Components.Exception(
      'Unimplemented method: ' + methodName, Cr.NS_ERROR_FAILURE);
  };
}

/**
 * The BrowserElementProxy talks to the Browser IFrameElement instance on
 * behave of the embedded document. It implements all the methods on
 * the Browser IFrameElement and the methods will work if they are applicable.
 *
 * The message is forwarded to BrowserElementParent.js by creating an
 * 'browser-element-api:proxy-call' observer message.
 * BrowserElementChildPreload will get notified and send the message through
 * to the main process through sendAsyncMessage with message of the same name.
 *
 * The return message will follow the same route. The message name on the
 * return route is 'browser-element-api:proxy'.
 *
 * Both BrowserElementProxy and BrowserElementParent must be modified if there
 * is a new method implemented, or a new event added to the Browser
 * IFrameElement.
 *
 * Other details unmentioned here are checks of message sender and recipients
 * to identify proxy instance on different innerWindows or ensure the content
 * process has the right permission.
 */
function BrowserElementProxy() {
  // Pad the 0th element so that DOMRequest ID will always be a truthy value.
  this._pendingDOMRequests = [ undefined ];
}

BrowserElementProxy.prototype = {
  classDescription: 'BrowserElementProxy allowed embedded frame to control ' +
                    'it\'s own embedding browser element frame instance.',
  classID:          Components.ID('{7e95d54c-9930-49c8-9a10-44fe40fe8251}'),
  contractID:       '@mozilla.org/dom/browser-element-proxy;1',

  QueryInterface: XPCOMUtils.generateQI([
    Ci.nsIDOMGlobalPropertyInitializer,
    Ci.nsIObserver]),

  _window: null,
  _innerWindowID: undefined,

  get allowedAudioChannels() {
    return this._window.navigator.mozAudioChannelManager ?
      this._window.navigator.mozAudioChannelManager.allowedAudioChannels :
      null;
  },

  init: function(win) {
    this._window = win;
    this._innerWindowID = win.QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsIDOMWindowUtils)
                          .currentInnerWindowID;

    this._sendToParent('_proxyInstanceInit');
    Services.obs.addObserver(this, 'browser-element-api:proxy', false);
  },

  uninit: function(win) {
    this._sendToParent('_proxyInstanceUninit');

    this._window = null;
    this._innerWindowID = undefined;

    Services.obs.removeObserver(this, 'browser-element-api:proxy');
  },

  observe: function(subject, topic, stringifedData) {
    let data = JSON.parse(stringifedData);

    if (subject !== this._window ||
        data.innerWindowID !== data.innerWindowID) {
      return;
    }

    if (data.eventName) {
      this._fireEvent(data.eventName, JSON.parse(data.eventDetailString));

      return;
    }

    if ('domRequestId' in data) {
      let req = this._pendingDOMRequests[data.domRequestId];
      this._pendingDOMRequests[data.domRequestId] = undefined;

      if (!req) {
        dump('BrowserElementProxy Error: ' +
          'Multiple observer messages for the same DOMRequest result.\n');
        return;
      }

      if ('result' in data) {
        let clientObj = Cu.cloneInto(data.result, this._window);
        Services.DOMRequest.fireSuccess(req, clientObj);
      } else {
        let clientObj = Cu.cloneInto(data.error, this._window);
        Services.DOMRequest.fireSuccess(req, clientObj);
      }

      return;
    }

    dump('BrowserElementProxy Error: ' +
          'Received unhandled observer messages ' + stringifedData + '.\n');
  },

  _sendDOMRequest: function(methodName, args) {
    let id = this._pendingDOMRequests.length;
    let req = Services.DOMRequest.createRequest(this._window);

    this._pendingDOMRequests.push(req);
    this._sendToParent(methodName, args, id);

    return req;
  },

  _sendToParent: function(methodName, args, domRequestId) {
    let data = {
      methodName: methodName,
      args: args,
      innerWindowID: this._innerWindowID
    };

    if (domRequestId) {
      data.domRequestId = domRequestId;
    }

    Services.obs.notifyObservers(
      this._window, 'browser-element-api:proxy-call', JSON.stringify(data));
  },

  _fireEvent: function(name, detail) {
    let evt = this._createEvent(name, detail,
                                /* cancelable = */ false);
    this.__DOM_IMPL__.dispatchEvent(evt);
  },

  _createEvent: function(evtName, detail, cancelable) {
    // This will have to change if we ever want to send a CustomEvent with null
    // detail.  For now, it's OK.
    if (detail !== undefined && detail !== null) {
      detail = Cu.cloneInto(detail, this._window);
      return new this._window.CustomEvent(evtName,
                                          { bubbles: false,
                                            cancelable: cancelable,
                                            detail: detail });
    }

    return new this._window.Event(evtName,
                                  { bubbles: false,
                                    cancelable: cancelable });
  },

  setVisible: defineNoReturnMethod('setVisible'),
  setActive: defineNoReturnMethod('setActive'),
  sendMouseEvent: defineNoReturnMethod('sendMouseEvent'),
  sendTouchEvent: defineNoReturnMethod('sendTouchEvent'),
  goBack: defineNoReturnMethod('goBack'),
  goForward: defineNoReturnMethod('goForward'),
  reload: defineNoReturnMethod('reload'),
  stop: defineNoReturnMethod('stop'),
  zoom: defineNoReturnMethod('zoom'),
  findAll: defineNoReturnMethod('findAll'),
  findNext: defineNoReturnMethod('findNext'),
  clearMatch: defineNoReturnMethod('clearMatch'),
  mute: defineNoReturnMethod('mute'),
  unmute: defineNoReturnMethod('unmute'),
  setVolume: defineNoReturnMethod('setVolume'),

  getVisible: defineDOMRequestMethod('getVisible'),
  download: defineDOMRequestMethod('download'),
  purgeHistory: defineDOMRequestMethod('purgeHistory'),
  getCanGoBack: defineDOMRequestMethod('getCanGoBack'),
  getCanGoForward: defineDOMRequestMethod('getCanGoForward'),
  getContentDimensions: defineDOMRequestMethod('getContentDimensions'),
  setInputMethodActive: defineDOMRequestMethod('setInputMethodActive'),
  executeScript: defineDOMRequestMethod('executeScript'),
  getMuted: defineDOMRequestMethod('getMuted'),
  getVolume: defineDOMRequestMethod('getVolume'),

  getActive: defineUnimplementedMethod('getActive'),
  addNextPaintListener: defineUnimplementedMethod('addNextPaintListener'),
  removeNextPaintListener: defineUnimplementedMethod('removeNextPaintListener'),
  getScreenshot: defineUnimplementedMethod('getScreenshot')
};

this.NSGetFactory = XPCOMUtils.generateNSGetFactory([BrowserElementProxy]);