/* 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");
Cu.import("resource://gre/modules/DOMRequestHelper.jsm");

XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
  "@mozilla.org/childprocessmessagemanager;1", "nsISyncMessageSender");

XPCOMUtils.defineLazyServiceGetter(this, "tm",
  "@mozilla.org/thread-manager;1", "nsIThreadManager");

/*
 * A WeakMap to map input method iframe window to
 * it's active status, kbID, and ipcHelper.
 */
var WindowMap = {
  // WeakMap of <window, object> pairs.
  _map: null,

  /*
   * Set the object associated to the window and return it.
   */
  _getObjForWin: function(win) {
    if (!this._map) {
      this._map = new WeakMap();
    }
    if (this._map.has(win)) {
      return this._map.get(win);
    } else {
      let obj = {
        active: false,
        kbID: undefined,
        ipcHelper: null
      };
      this._map.set(win, obj);

      return obj;
    }
  },

  /*
   * Check if the given window is active.
   */
  isActive: function(win) {
    if (!this._map || !win) {
      return false;
    }

    return this._getObjForWin(win).active;
  },

  /*
   * Set the active status of the given window.
   */
  setActive: function(win, isActive) {
    if (!win) {
      return;
    }
    let obj = this._getObjForWin(win);
    obj.active = isActive;
  },

  /*
   * Get the keyboard ID (assigned by Keyboard.jsm) of the given window.
   */
  getKbID: function(win) {
    if (!this._map || !win) {
      return undefined;
    }

    let obj = this._getObjForWin(win);
    return obj.kbID;
  },

  /*
   * Set the keyboard ID (assigned by Keyboard.jsm) of the given window.
   */
  setKbID: function(win, kbID) {
    if (!win) {
      return;
    }
    let obj = this._getObjForWin(win);
    obj.kbID = kbID;
  },

  /*
   * Get InputContextDOMRequestIpcHelper instance attached to this window.
   */
  getInputContextIpcHelper: function(win) {
    if (!win) {
      return;
    }
    let obj = this._getObjForWin(win);
    if (!obj.ipcHelper) {
      obj.ipcHelper = new InputContextDOMRequestIpcHelper(win);
    }
    return obj.ipcHelper;
  },

  /*
   * Unset InputContextDOMRequestIpcHelper instance.
   */
  unsetInputContextIpcHelper: function(win) {
    if (!win) {
      return;
    }
    let obj = this._getObjForWin(win);
    if (!obj.ipcHelper) {
      return;
    }
    obj.ipcHelper = null;
  }
};

var cpmmSendAsyncMessageWithKbID = function (self, msg, data) {
  data.kbID = WindowMap.getKbID(self._window);
  cpmm.sendAsyncMessage(msg, data);
};

/**
 * ==============================================
 * InputMethodManager
 * ==============================================
 */
function MozInputMethodManager(win) {
  this._window = win;
}

MozInputMethodManager.prototype = {
  supportsSwitchingForCurrentInputContext: false,
  _window: null,

  classID: Components.ID("{7e9d7280-ef86-11e2-b778-0800200c9a66}"),

  QueryInterface: XPCOMUtils.generateQI([]),

  set oninputcontextfocus(handler) {
    this.__DOM_IMPL__.setEventHandler("oninputcontextfocus", handler);
  },

  get oninputcontextfocus() {
    return this.__DOM_IMPL__.getEventHandler("oninputcontextfocus");
  },

  set oninputcontextblur(handler) {
    this.__DOM_IMPL__.setEventHandler("oninputcontextblur", handler);
  },

  get oninputcontextblur() {
    return this.__DOM_IMPL__.getEventHandler("oninputcontextblur");
  },

  set onshowallrequest(handler) {
    this.__DOM_IMPL__.setEventHandler("onshowallrequest", handler);
  },

  get onshowallrequest() {
    return this.__DOM_IMPL__.getEventHandler("onshowallrequest");
  },

  set onnextrequest(handler) {
    this.__DOM_IMPL__.setEventHandler("onnextrequest", handler);
  },

  get onnextrequest() {
    return this.__DOM_IMPL__.getEventHandler("onnextrequest");
  },

  set onaddinputrequest(handler) {
    this.__DOM_IMPL__.setEventHandler("onaddinputrequest", handler);
  },

  get onaddinputrequest() {
    return this.__DOM_IMPL__.getEventHandler("onaddinputrequest");
  },

  set onremoveinputrequest(handler) {
    this.__DOM_IMPL__.setEventHandler("onremoveinputrequest", handler);
  },

  get onremoveinputrequest() {
    return this.__DOM_IMPL__.getEventHandler("onremoveinputrequest");
  },

  showAll: function() {
    if (!WindowMap.isActive(this._window)) {
      return;
    }
    cpmmSendAsyncMessageWithKbID(this, 'Keyboard:ShowInputMethodPicker', {});
  },

  next: function() {
    if (!WindowMap.isActive(this._window)) {
      return;
    }
    cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SwitchToNextInputMethod', {});
  },

  supportsSwitching: function() {
    if (!WindowMap.isActive(this._window)) {
      return false;
    }
    return this.supportsSwitchingForCurrentInputContext;
  },

  hide: function() {
    if (!WindowMap.isActive(this._window)) {
      return;
    }
    cpmmSendAsyncMessageWithKbID(this, 'Keyboard:RemoveFocus', {});
  },

  setSupportsSwitchingTypes: function(types) {
    cpmm.sendAsyncMessage('System:SetSupportsSwitchingTypes', {
      types: types
    });
  },

  handleFocus: function(data) {
    let detail = new MozInputContextFocusEventDetail(this._window, data);
    let wrappedDetail =
      this._window.MozInputContextFocusEventDetail._create(this._window, detail);
    let event = new this._window.CustomEvent('inputcontextfocus',
      { cancelable: true, detail: wrappedDetail });

    let handled = !this.__DOM_IMPL__.dispatchEvent(event);

    // A gentle warning if the event is not preventDefault() by the content.
    if (!handled) {
      dump('MozKeyboard.js: A frame with input-manage permission did not' +
        ' handle the inputcontextfocus event dispatched.\n');
    }
  },

  handleBlur: function(data) {
    let event =
      new this._window.Event('inputcontextblur', { cancelable: true });

    let handled = !this.__DOM_IMPL__.dispatchEvent(event);

    // A gentle warning if the event is not preventDefault() by the content.
    if (!handled) {
      dump('MozKeyboard.js: A frame with input-manage permission did not' +
        ' handle the inputcontextblur event dispatched.\n');
    }
  },

  dispatchShowAllRequestEvent: function() {
    this._fireSimpleEvent('showallrequest');
  },

  dispatchNextRequestEvent: function() {
    this._fireSimpleEvent('nextrequest');
  },

  _fireSimpleEvent: function(eventType) {
    let event = new this._window.Event(eventType);
    let handled = !this.__DOM_IMPL__.dispatchEvent(event, { cancelable: true });

    // A gentle warning if the event is not preventDefault() by the content.
    if (!handled) {
      dump('MozKeyboard.js: A frame with input-manage permission did not' +
        ' handle the ' + eventType + ' event dispatched.\n');
    }
  },

  handleAddInput: function(data) {
    let p = this._fireInputRegistryEvent('addinputrequest', data);
    if (!p) {
      return;
    }

    p.then(() => {
      cpmm.sendAsyncMessage('System:InputRegistry:Add:Done', {
        id: data.id
      });
    }, (error) => {
      cpmm.sendAsyncMessage('System:InputRegistry:Add:Done', {
        id: data.id,
        error: error || 'Unknown Error'
      });
    });
  },

  handleRemoveInput: function(data) {
    let p = this._fireInputRegistryEvent('removeinputrequest', data);
    if (!p) {
      return;
    }

    p.then(() => {
      cpmm.sendAsyncMessage('System:InputRegistry:Remove:Done', {
        id: data.id
      });
    }, (error) => {
      cpmm.sendAsyncMessage('System:InputRegistry:Remove:Done', {
        id: data.id,
        error: error || 'Unknown Error'
      });
    });
  },

  _fireInputRegistryEvent: function(eventType, data) {
    let detail = new MozInputRegistryEventDetail(this._window, data);
    let wrappedDetail =
      this._window.MozInputRegistryEventDetail._create(this._window, detail);
    let event = new this._window.CustomEvent(eventType,
      { cancelable: true, detail: wrappedDetail });
    let handled = !this.__DOM_IMPL__.dispatchEvent(event);

    // A gentle warning if the event is not preventDefault() by the content.
    if (!handled) {
      dump('MozKeyboard.js: A frame with input-manage permission did not' +
        ' handle the ' + eventType + ' event dispatched.\n');

      return null;
    }
    return detail.takeChainedPromise();
  }
};

function MozInputContextFocusEventDetail(win, data) {
  this.type = data.type;
  this.inputType = data.inputType;
  this.value = data.value;
  // Exposed as MozInputContextChoicesInfo dictionary defined in WebIDL
  this.choices = data.choices;
  this.min = data.min;
  this.max = data.max;
}
MozInputContextFocusEventDetail.prototype = {
  classID: Components.ID("{e0794208-ac50-40e8-b22e-6ee0b4c4e6e8}"),
  QueryInterface: XPCOMUtils.generateQI([]),

  type: undefined,
  inputType: undefined,
  value: '',
  choices: null,
  min: undefined,
  max: undefined
};

function MozInputRegistryEventDetail(win, data) {
  this._window = win;

  this.manifestURL = data.manifestURL;
  this.inputId = data.inputId;
  // Exposed as MozInputMethodInputManifest dictionary defined in WebIDL
  this.inputManifest = data.inputManifest;

  this._chainedPromise = Promise.resolve();
}
MozInputRegistryEventDetail.prototype = {
  classID: Components.ID("{02130070-9b3e-4f38-bbd9-f0013aa36717}"),
  QueryInterface: XPCOMUtils.generateQI([]),

  _window: null,

  manifestURL: undefined,
  inputId: undefined,
  inputManifest: null,

  waitUntil: function(p) {
    // Need an extra protection here since waitUntil will be an no-op
    // when chainedPromise is already returned.
    if (!this._chainedPromise) {
      throw new this._window.DOMException(
        'Must call waitUntil() within the event handling loop.',
        'InvalidStateError');
    }

    this._chainedPromise = this._chainedPromise
      .then(function() { return p; });
  },

  takeChainedPromise: function() {
    var p = this._chainedPromise;
    this._chainedPromise = null;
    return p;
  }
};

/**
 * ==============================================
 * InputMethod
 * ==============================================
 */
function MozInputMethod() { }

MozInputMethod.prototype = {
  __proto__: DOMRequestIpcHelper.prototype,

  _window: null,
  _inputcontext: null,
  _wrappedInputContext: null,
  _mgmt: null,
  _wrappedMgmt: null,
  _supportsSwitchingTypes: [],
  _inputManageId: undefined,

  classID: Components.ID("{4607330d-e7d2-40a4-9eb8-43967eae0142}"),

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

  init: function mozInputMethodInit(win) {
    this._window = win;
    this._mgmt = new MozInputMethodManager(win);
    this._wrappedMgmt = win.MozInputMethodManager._create(win, this._mgmt);
    this.innerWindowID = win.QueryInterface(Ci.nsIInterfaceRequestor)
                            .getInterface(Ci.nsIDOMWindowUtils)
                            .currentInnerWindowID;

    Services.obs.addObserver(this, "inner-window-destroyed", false);

    cpmm.addWeakMessageListener('Keyboard:Focus', this);
    cpmm.addWeakMessageListener('Keyboard:Blur', this);
    cpmm.addWeakMessageListener('Keyboard:SelectionChange', this);
    cpmm.addWeakMessageListener('Keyboard:GetContext:Result:OK', this);
    cpmm.addWeakMessageListener('Keyboard:SupportsSwitchingTypesChange', this);
    cpmm.addWeakMessageListener('Keyboard:ReceiveHardwareKeyEvent', this);
    cpmm.addWeakMessageListener('InputRegistry:Result:OK', this);
    cpmm.addWeakMessageListener('InputRegistry:Result:Error', this);

    if (this._hasInputManagePerm(win)) {
      this._inputManageId = cpmm.sendSyncMessage('System:RegisterSync', {})[0];
      cpmm.addWeakMessageListener('System:Focus', this);
      cpmm.addWeakMessageListener('System:Blur', this);
      cpmm.addWeakMessageListener('System:ShowAll', this);
      cpmm.addWeakMessageListener('System:Next', this);
      cpmm.addWeakMessageListener('System:InputRegistry:Add', this);
      cpmm.addWeakMessageListener('System:InputRegistry:Remove', this);
    }
  },

  uninit: function mozInputMethodUninit() {
    this._window = null;
    this._mgmt = null;
    this._wrappedMgmt = null;

    cpmm.removeWeakMessageListener('Keyboard:Focus', this);
    cpmm.removeWeakMessageListener('Keyboard:Blur', this);
    cpmm.removeWeakMessageListener('Keyboard:SelectionChange', this);
    cpmm.removeWeakMessageListener('Keyboard:GetContext:Result:OK', this);
    cpmm.removeWeakMessageListener('Keyboard:SupportsSwitchingTypesChange', this);
    cpmm.removeWeakMessageListener('Keyboard:ReceiveHardwareKeyEvent', this);
    cpmm.removeWeakMessageListener('InputRegistry:Result:OK', this);
    cpmm.removeWeakMessageListener('InputRegistry:Result:Error', this);
    this.setActive(false);

    if (typeof this._inputManageId === 'number') {
      cpmm.sendAsyncMessage('System:Unregister', {
        'id': this._inputManageId
      });
      cpmm.removeWeakMessageListener('System:Focus', this);
      cpmm.removeWeakMessageListener('System:Blur', this);
      cpmm.removeWeakMessageListener('System:ShowAll', this);
      cpmm.removeWeakMessageListener('System:Next', this);
      cpmm.removeWeakMessageListener('System:InputRegistry:Add', this);
      cpmm.removeWeakMessageListener('System:InputRegistry:Remove', this);
    }
  },

  receiveMessage: function mozInputMethodReceiveMsg(msg) {
    if (msg.name.startsWith('Keyboard') &&
        !WindowMap.isActive(this._window)) {
      return;
    }

    let data = msg.data;

    if (msg.name.startsWith('System') &&
      this._inputManageId !== data.inputManageId) {
      return;
    }
    delete data.inputManageId;

    let resolver = ('requestId' in data) ?
      this.takePromiseResolver(data.requestId) : null;

    switch(msg.name) {
      case 'Keyboard:Focus':
        // XXX Bug 904339 could receive 'text' event twice
        this.setInputContext(data);
        break;
      case 'Keyboard:Blur':
        this.setInputContext(null);
        break;
      case 'Keyboard:SelectionChange':
        if (this.inputcontext) {
          this._inputcontext.updateSelectionContext(data, false);
        }
        break;
      case 'Keyboard:GetContext:Result:OK':
        this.setInputContext(data);
        break;
      case 'Keyboard:SupportsSwitchingTypesChange':
        this._supportsSwitchingTypes = data.types;
        break;
      case 'Keyboard:ReceiveHardwareKeyEvent':
        if (!Ci.nsIHardwareKeyHandler) {
          break;
        }

        let defaultPrevented = Ci.nsIHardwareKeyHandler.NO_DEFAULT_PREVENTED;

        // |event.preventDefault()| is allowed to be called only when
        // |event.cancelable| is true
        if (this._inputcontext && data.keyDict.cancelable) {
          defaultPrevented |= this._inputcontext.forwardHardwareKeyEvent(data);
        }

        cpmmSendAsyncMessageWithKbID(this, 'Keyboard:ReplyHardwareKeyEvent', {
                                       type: data.type,
                                       defaultPrevented: defaultPrevented
                                     });
        break;
      case 'InputRegistry:Result:OK':
        resolver.resolve();

        break;

      case 'InputRegistry:Result:Error':
        resolver.reject(data.error);

        break;

      case 'System:Focus':
        this._mgmt.handleFocus(data);
        break;

      case 'System:Blur':
        this._mgmt.handleBlur(data);
        break;

      case 'System:ShowAll':
        this._mgmt.dispatchShowAllRequestEvent();
        break;

      case 'System:Next':
        this._mgmt.dispatchNextRequestEvent();
        break;

      case 'System:InputRegistry:Add':
        this._mgmt.handleAddInput(data);
        break;

      case 'System:InputRegistry:Remove':
        this._mgmt.handleRemoveInput(data);
        break;
    }
  },

  observe: function mozInputMethodObserve(subject, topic, data) {
    let wId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
    if (wId == this.innerWindowID)
      this.uninit();
  },

  get mgmt() {
    return this._wrappedMgmt;
  },

  get inputcontext() {
    if (!WindowMap.isActive(this._window)) {
      return null;
    }
    return this._wrappedInputContext;
  },

  set oninputcontextchange(handler) {
    this.__DOM_IMPL__.setEventHandler("oninputcontextchange", handler);
  },

  get oninputcontextchange() {
    return this.__DOM_IMPL__.getEventHandler("oninputcontextchange");
  },

  setInputContext: function mozKeyboardContextChange(data) {
    if (this._inputcontext) {
      this._inputcontext.destroy();
      this._inputcontext = null;
      this._wrappedInputContext = null;
      this._mgmt.supportsSwitchingForCurrentInputContext = false;
    }

    if (data) {
      this._mgmt.supportsSwitchingForCurrentInputContext =
        (this._supportsSwitchingTypes.indexOf(data.inputType) !== -1);

      this._inputcontext = new MozInputContext(data);
      this._inputcontext.init(this._window);
      // inputcontext will be exposed as a WebIDL object. Create its
      // content-side object explicitly to avoid Bug 1001325.
      this._wrappedInputContext =
        this._window.MozInputContext._create(this._window, this._inputcontext);
    }

    let event = new this._window.Event("inputcontextchange");
    this.__DOM_IMPL__.dispatchEvent(event);
  },

  setActive: function mozInputMethodSetActive(isActive) {
    if (WindowMap.isActive(this._window) === isActive) {
      return;
    }

    WindowMap.setActive(this._window, isActive);

    if (isActive) {
      // Activate current input method.
      // If there is already an active context, then this will trigger
      // a GetContext:Result:OK event, and we can initialize ourselves.
      // Otherwise silently ignored.

      // get keyboard ID from Keyboard.jsm,
      // or if we already have it, get it from our map
      // Note: if we need to get it from Keyboard.jsm,
      // we have to use a synchronous message
      var kbID = WindowMap.getKbID(this._window);
      if (kbID) {
        cpmmSendAsyncMessageWithKbID(this, 'Keyboard:RegisterSync', {});
      } else {
        let res = cpmm.sendSyncMessage('Keyboard:RegisterSync', {});
        WindowMap.setKbID(this._window, res[0]);
      }

      cpmmSendAsyncMessageWithKbID(this, 'Keyboard:GetContext', {});
    } else {
      // Deactive current input method.
      cpmmSendAsyncMessageWithKbID(this, 'Keyboard:Unregister', {});
      if (this._inputcontext) {
        this.setInputContext(null);
      }
    }
  },

  addInput: function(inputId, inputManifest) {
    return this.createPromiseWithId(function(resolverId) {
      let appId = this._window.document.nodePrincipal.appId;

      cpmm.sendAsyncMessage('InputRegistry:Add', {
        requestId: resolverId,
        inputId: inputId,
        inputManifest: inputManifest,
        appId: appId
      });
    }.bind(this));
  },

  removeInput: function(inputId) {
    return this.createPromiseWithId(function(resolverId) {
      let appId = this._window.document.nodePrincipal.appId;

      cpmm.sendAsyncMessage('InputRegistry:Remove', {
        requestId: resolverId,
        inputId: inputId,
        appId: appId
      });
    }.bind(this));
  },

  setValue: function(value) {
    cpmm.sendAsyncMessage('System:SetValue', {
      'value': value
    });
  },

  setSelectedOption: function(index) {
    cpmm.sendAsyncMessage('System:SetSelectedOption', {
      'index': index
    });
  },

  setSelectedOptions: function(indexes) {
    cpmm.sendAsyncMessage('System:SetSelectedOptions', {
      'indexes': indexes
    });
  },

  removeFocus: function() {
    cpmm.sendAsyncMessage('System:RemoveFocus', {});
  },

  // Only the system app needs that, so instead of testing a permission which
  // is allowed for all chrome:// url, we explicitly test that this is the
  // system app's start URL.
  _hasInputManagePerm: function(win) {
    let url = win.location.href;
    let systemAppIndex;
    try {
      systemAppIndex = Services.prefs.getCharPref('b2g.system_startup_url');
    } catch(e) {
      dump('MozKeyboard.jsm: no system app startup url set (pref is b2g.system_startup_url)');
    }

    dump(`MozKeyboard.jsm expecting ${systemAppIndex}\n`);
    return url == systemAppIndex;
  }
};

/**
 * ==============================================
 * InputContextDOMRequestIpcHelper
 * ==============================================
 */
function InputContextDOMRequestIpcHelper(win) {
  this.initDOMRequestHelper(win,
    ["Keyboard:GetText:Result:OK",
     "Keyboard:GetText:Result:Error",
     "Keyboard:SetSelectionRange:Result:OK",
     "Keyboard:ReplaceSurroundingText:Result:OK",
     "Keyboard:SendKey:Result:OK",
     "Keyboard:SendKey:Result:Error",
     "Keyboard:SetComposition:Result:OK",
     "Keyboard:EndComposition:Result:OK",
     "Keyboard:SequenceError"]);
}

InputContextDOMRequestIpcHelper.prototype = {
  __proto__: DOMRequestIpcHelper.prototype,
  _inputContext: null,

  attachInputContext: function(inputCtx) {
    if (this._inputContext) {
      throw new Error("InputContextDOMRequestIpcHelper: detach the context first.");
    }

    this._inputContext = inputCtx;
  },

  // Unset ourselves when the window is destroyed.
  uninit: function() {
    WindowMap.unsetInputContextIpcHelper(this._window);
  },

  detachInputContext: function() {
    // All requests that are still pending need to be invalidated
    // because the context is no longer valid.
    this.forEachPromiseResolver(k => {
      this.takePromiseResolver(k).reject("InputContext got destroyed");
    });

    this._inputContext = null;
  },

  receiveMessage: function(msg) {
    if (!this._inputContext) {
      dump('InputContextDOMRequestIpcHelper received message without context attached.\n');
      return;
    }

    this._inputContext.receiveMessage(msg);
  }
};

function MozInputContextSelectionChangeEventDetail(ctx, ownAction) {
  this._ctx = ctx;
  this.ownAction = ownAction;
}

MozInputContextSelectionChangeEventDetail.prototype = {
  classID: Components.ID("ef35443e-a400-4ae3-9170-c2f4e05f7aed"),
  QueryInterface: XPCOMUtils.generateQI([]),

  ownAction: false,

  get selectionStart() {
    return this._ctx.selectionStart;
  },

  get selectionEnd() {
    return this._ctx.selectionEnd;
  }
};

function MozInputContextSurroundingTextChangeEventDetail(ctx, ownAction) {
  this._ctx = ctx;
  this.ownAction = ownAction;
}

MozInputContextSurroundingTextChangeEventDetail.prototype = {
  classID: Components.ID("1c50fdaf-74af-4b2e-814f-792caf65a168"),
  QueryInterface: XPCOMUtils.generateQI([]),

  ownAction: false,

  get text() {
    return this._ctx.text;
  },

  get textBeforeCursor() {
    return this._ctx.textBeforeCursor;
  },

  get textAfterCursor() {
    return this._ctx.textAfterCursor;
  }
};

/**
 * ==============================================
 * HardwareInput
 * ==============================================
 */
function MozHardwareInput() {
}

MozHardwareInput.prototype = {
  classID: Components.ID("{1e38633d-d08b-4867-9944-afa5c648adb6}"),
  QueryInterface: XPCOMUtils.generateQI([]),
};

/**
 * ==============================================
 * InputContext
 * ==============================================
 */
function MozInputContext(data) {
  this._context = {
    type: data.type,
    inputType: data.inputType,
    inputMode: data.inputMode,
    lang: data.lang,
    selectionStart: data.selectionStart,
    selectionEnd: data.selectionEnd,
    text: data.value
  };

  this._contextId = data.contextId;
}

MozInputContext.prototype = {
  _window: null,
  _context: null,
  _contextId: -1,
  _ipcHelper: null,
  _hardwareinput: null,
  _wrappedhardwareinput: null,

  classID: Components.ID("{1e38633d-d08b-4867-9944-afa5c648adb6}"),

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

  init: function ic_init(win) {
    this._window = win;

    this._ipcHelper = WindowMap.getInputContextIpcHelper(win);
    this._ipcHelper.attachInputContext(this);
    this._hardwareinput = new MozHardwareInput();
    this._wrappedhardwareinput =
      this._window.MozHardwareInput._create(this._window, this._hardwareinput);
  },

  destroy: function ic_destroy() {
    // A consuming application might still hold a cached version of
    // this object. After destroying all methods will throw because we
    // cannot create new promises anymore, but we still hold
    // (outdated) information in the context. So let's clear that out.
    for (var k in this._context) {
      if (this._context.hasOwnProperty(k)) {
        this._context[k] = null;
      }
    }

    this._ipcHelper.detachInputContext();
    this._ipcHelper = null;

    this._window = null;
    this._hardwareinput = null;
    this._wrappedhardwareinput = null;
  },

  receiveMessage: function ic_receiveMessage(msg) {
    if (!msg || !msg.json) {
      dump('InputContext received message without data\n');
      return;
    }

    let json = msg.json;
    let resolver = this._ipcHelper.takePromiseResolver(json.requestId);

    if (!resolver) {
      dump('InputContext received invalid requestId.\n');
      return;
    }

    // Update context first before resolving promise to avoid race condition
    if (json.selectioninfo) {
      this.updateSelectionContext(json.selectioninfo, true);
    }

    switch (msg.name) {
      case "Keyboard:SendKey:Result:OK":
        resolver.resolve(true);
        break;
      case "Keyboard:SendKey:Result:Error":
        resolver.reject(json.error);
        break;
      case "Keyboard:GetText:Result:OK":
        resolver.resolve(json.text);
        break;
      case "Keyboard:GetText:Result:Error":
        resolver.reject(json.error);
        break;
      case "Keyboard:SetSelectionRange:Result:OK":
      case "Keyboard:ReplaceSurroundingText:Result:OK":
        resolver.resolve(
          Cu.cloneInto(json.selectioninfo, this._window));
        break;
      case "Keyboard:SequenceError":
        // Occurs when a new element got focus, but the inputContext was
        // not invalidated yet...
        resolver.reject("InputContext has expired");
        break;
      case "Keyboard:SetComposition:Result:OK": // Fall through.
      case "Keyboard:EndComposition:Result:OK":
        resolver.resolve(true);
        break;
      default:
        dump("Could not find a handler for " + msg.name);
        resolver.reject();
        break;
    }
  },

  updateSelectionContext: function ic_updateSelectionContext(data, ownAction) {
    if (!this._context) {
      return;
    }

    let selectionDirty =
      this._context.selectionStart !== data.selectionStart ||
      this._context.selectionEnd !== data.selectionEnd;
    let surroundDirty = selectionDirty || data.text !== this._contextId.text;

    this._context.text = data.text;
    this._context.selectionStart = data.selectionStart;
    this._context.selectionEnd = data.selectionEnd;

    if (selectionDirty) {
      let selectionChangeDetail =
        new MozInputContextSelectionChangeEventDetail(this, ownAction);
      let wrappedSelectionChangeDetail =
        this._window.MozInputContextSelectionChangeEventDetail
          ._create(this._window, selectionChangeDetail);
      let selectionChangeEvent = new this._window.CustomEvent("selectionchange",
        { cancelable: false, detail: wrappedSelectionChangeDetail });

      this.__DOM_IMPL__.dispatchEvent(selectionChangeEvent);
    }

    if (surroundDirty) {
      let surroundingTextChangeDetail =
        new MozInputContextSurroundingTextChangeEventDetail(this, ownAction);
      let wrappedSurroundingTextChangeDetail =
        this._window.MozInputContextSurroundingTextChangeEventDetail
          ._create(this._window, surroundingTextChangeDetail);
      let selectionChangeEvent = new this._window.CustomEvent("surroundingtextchange",
        { cancelable: false, detail: wrappedSurroundingTextChangeDetail });

      this.__DOM_IMPL__.dispatchEvent(selectionChangeEvent);
    }
  },

  // tag name of the input field
  get type() {
    return this._context.type;
  },

  // type of the input field
  get inputType() {
    return this._context.inputType;
  },

  get inputMode() {
    return this._context.inputMode;
  },

  get lang() {
    return this._context.lang;
  },

  getText: function ic_getText(offset, length) {
    let text;
    if (offset && length) {
      text = this._context.text.substr(offset, length);
    } else if (offset) {
      text = this._context.text.substr(offset);
    } else {
      text = this._context.text;
    }

    return this._window.Promise.resolve(text);
  },

  get selectionStart() {
    return this._context.selectionStart;
  },

  get selectionEnd() {
    return this._context.selectionEnd;
  },

  get text() {
    return this._context.text;
  },

  get textBeforeCursor() {
    let text = this._context.text;
    let start = this._context.selectionStart;
    return (start < 100) ?
      text.substr(0, start) :
      text.substr(start - 100, 100);
  },

  get textAfterCursor() {
    let text = this._context.text;
    let start = this._context.selectionStart;
    let end = this._context.selectionEnd;
    return text.substr(start, end - start + 100);
  },

  get hardwareinput() {
    return this._wrappedhardwareinput;
  },

  setSelectionRange: function ic_setSelectionRange(start, length) {
    let self = this;
    return this._sendPromise(function(resolverId) {
      cpmmSendAsyncMessageWithKbID(self, 'Keyboard:SetSelectionRange', {
        contextId: self._contextId,
        requestId: resolverId,
        selectionStart: start,
        selectionEnd: start + length
      });
    });
  },

  get onsurroundingtextchange() {
    return this.__DOM_IMPL__.getEventHandler("onsurroundingtextchange");
  },

  set onsurroundingtextchange(handler) {
    this.__DOM_IMPL__.setEventHandler("onsurroundingtextchange", handler);
  },

  get onselectionchange() {
    return this.__DOM_IMPL__.getEventHandler("onselectionchange");
  },

  set onselectionchange(handler) {
    this.__DOM_IMPL__.setEventHandler("onselectionchange", handler);
  },

  replaceSurroundingText: function ic_replaceSurrText(text, offset, length) {
    let self = this;
    return this._sendPromise(function(resolverId) {
      cpmmSendAsyncMessageWithKbID(self, 'Keyboard:ReplaceSurroundingText', {
        contextId: self._contextId,
        requestId: resolverId,
        text: text,
        offset: offset || 0,
        length: length || 0
      });
    });
  },

  deleteSurroundingText: function ic_deleteSurrText(offset, length) {
    return this.replaceSurroundingText(null, offset, length);
  },

  sendKey: function ic_sendKey(dictOrKeyCode, charCode, modifiers, repeat) {
    if (typeof dictOrKeyCode === 'number') {
      // XXX: modifiers are ignored in this API method.

      return this._sendPromise((resolverId) => {
        cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SendKey', {
          contextId: this._contextId,
          requestId: resolverId,
          method: 'sendKey',
          keyCode: dictOrKeyCode,
          charCode: charCode,
          repeat: repeat
        });
      });
    } else if (typeof dictOrKeyCode === 'object') {
      return this._sendPromise((resolverId) => {
        cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SendKey', {
          contextId: this._contextId,
          requestId: resolverId,
          method: 'sendKey',
          keyboardEventDict: this._getkeyboardEventDict(dictOrKeyCode)
        });
      });
    } else {
      // XXX: Should not reach here; implies WebIDL binding error.
      throw new TypeError('Unknown argument passed.');
    }
  },

  keydown: function ic_keydown(dict) {
    return this._sendPromise((resolverId) => {
      cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SendKey', {
        contextId: this._contextId,
         requestId: resolverId,
        method: 'keydown',
        keyboardEventDict: this._getkeyboardEventDict(dict)
       });
     });
   },

  keyup: function ic_keyup(dict) {
    return this._sendPromise((resolverId) => {
      cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SendKey', {
        contextId: this._contextId,
        requestId: resolverId,
        method: 'keyup',
        keyboardEventDict: this._getkeyboardEventDict(dict)
      });
    });
  },

  setComposition: function ic_setComposition(text, cursor, clauses, dict) {
    let self = this;
    return this._sendPromise((resolverId) => {
      cpmmSendAsyncMessageWithKbID(self, 'Keyboard:SetComposition', {
        contextId: self._contextId,
        requestId: resolverId,
        text: text,
        cursor: (typeof cursor !== 'undefined') ? cursor : text.length,
        clauses: clauses || null,
        keyboardEventDict: this._getkeyboardEventDict(dict)
      });
    });
  },

  endComposition: function ic_endComposition(text, dict) {
    let self = this;
    return this._sendPromise((resolverId) => {
      cpmmSendAsyncMessageWithKbID(self, 'Keyboard:EndComposition', {
        contextId: self._contextId,
        requestId: resolverId,
        text: text || '',
        keyboardEventDict: this._getkeyboardEventDict(dict)
      });
    });
  },

  // Generate a new keyboard event by the received keyboard dictionary
  // and return defaultPrevented's result of the event after dispatching.
  forwardHardwareKeyEvent: function ic_forwardHardwareKeyEvent(data) {
    if (!Ci.nsIHardwareKeyHandler) {
      return;
    }

    if (!this._context) {
      return Ci.nsIHardwareKeyHandler.NO_DEFAULT_PREVENTED;
    }
    let evt = new this._window.KeyboardEvent(data.type,
                                             Cu.cloneInto(data.keyDict,
                                                          this._window));
    this._hardwareinput.__DOM_IMPL__.dispatchEvent(evt);
    return this._getDefaultPreventedValue(evt);
  },

  _getDefaultPreventedValue: function(evt) {
    if (!Ci.nsIHardwareKeyHandler) {
      return;
    }

    let flags = Ci.nsIHardwareKeyHandler.NO_DEFAULT_PREVENTED;

    if (evt.defaultPrevented) {
      flags |= Ci.nsIHardwareKeyHandler.DEFAULT_PREVENTED;
    }

    if (evt.defaultPreventedByChrome) {
      flags |= Ci.nsIHardwareKeyHandler.DEFAULT_PREVENTED_BY_CHROME;
    }

    if (evt.defaultPreventedByContent) {
      flags |= Ci.nsIHardwareKeyHandler.DEFAULT_PREVENTED_BY_CONTENT;
    }

    return flags;
  },

  _sendPromise: function(callback) {
    let self = this;
    return this._ipcHelper.createPromiseWithId(function(aResolverId) {
      if (!WindowMap.isActive(self._window)) {
        self._ipcHelper.removePromiseResolver(aResolverId);
        reject('Input method is not active.');
        return;
      }
      callback(aResolverId);
    });
  },

  // Take a MozInputMethodKeyboardEventDict dict, creates a keyboardEventDict
  // object that can be sent to forms.js
  _getkeyboardEventDict: function(dict) {
    if (typeof dict !== 'object' || !dict.key) {
      return;
    }

    var keyboardEventDict = {
      key: dict.key,
      code: dict.code,
      repeat: dict.repeat,
      flags: 0
    };

    if (dict.printable) {
      keyboardEventDict.flags |=
        Ci.nsITextInputProcessor.KEY_FORCE_PRINTABLE_KEY;
    }

    if (/^[a-zA-Z0-9]$/.test(dict.key)) {
      // keyCode must follow the key value in this range;
      // disregard the keyCode from content.
      keyboardEventDict.keyCode = dict.key.toUpperCase().charCodeAt(0);
    } else if (typeof dict.keyCode === 'number') {
      // Allow keyCode to be specified for other key values.
      keyboardEventDict.keyCode = dict.keyCode;

      // Allow keyCode to be explicitly set to zero.
      if (dict.keyCode === 0) {
        keyboardEventDict.flags |=
          Ci.nsITextInputProcessor.KEY_KEEP_KEYCODE_ZERO;
      }
    }

    return keyboardEventDict;
  }
};

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