/* 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 DEBUG = false;
function debug(s) { dump("-*- ContactManager: " + s + "\n"); }

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;

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

XPCOMUtils.defineLazyGetter(Services, "DOMRequest", function() {
  return Cc["@mozilla.org/dom/dom-request-service;1"].getService(Ci.nsIDOMRequestService);
});

XPCOMUtils.defineLazyServiceGetter(this, "pm",
                                   "@mozilla.org/permissionmanager;1",
                                   "nsIPermissionManager");

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

const CONTACTS_SENDMORE_MINIMUM = 5;

function stringOrBust(aObj) {
  if (typeof aObj != "string") {
    if (DEBUG) debug("Field is not a string and was ignored.");
    return undefined;
  } else {
    return aObj;
  }
}

function sanitizeStringArray(aArray) {
  if (!Array.isArray(aArray)) {
    aArray = [aArray];
  }
  return aArray.map(stringOrBust).filter(function(el) { return el != undefined; });
}

const nsIClassInfo            = Ci.nsIClassInfo;
const CONTACTPROPERTIES_CID   = Components.ID("{35ad8a4e-9486-44b6-883d-550f14635e49}");
const nsIContactProperties    = Ci.nsIContactProperties;

// ContactProperties is not directly instantiated. It is used as interface.

function ContactProperties(aProp) { if (DEBUG) debug("ContactProperties Constructor"); }

ContactProperties.prototype = {

  classID : CONTACTPROPERTIES_CID,
  classInfo : XPCOMUtils.generateCI({classID: CONTACTPROPERTIES_CID,
                                     contractID:"@mozilla.org/contactProperties;1",
                                     classDescription: "ContactProperties",
                                     interfaces: [nsIContactProperties],
                                     flags: nsIClassInfo.DOM_OBJECT}),

  QueryInterface : XPCOMUtils.generateQI([nsIContactProperties])
}

//ContactAddress

const CONTACTADDRESS_CONTRACTID = "@mozilla.org/contactAddress;1";
const CONTACTADDRESS_CID        = Components.ID("{9cbfa81c-bcab-4ca9-b0d2-f4318f295e33}");
const nsIContactAddress         = Components.interfaces.nsIContactAddress;

function ContactAddress(aType, aStreetAddress, aLocality, aRegion, aPostalCode, aCountryName, aPref) {
  this.type = sanitizeStringArray(aType);
  this.streetAddress = stringOrBust(aStreetAddress);
  this.locality = stringOrBust(aLocality);
  this.region = stringOrBust(aRegion);
  this.postalCode = stringOrBust(aPostalCode);
  this.countryName = stringOrBust(aCountryName);
  this.pref = aPref;
};

ContactAddress.prototype = {
  __exposedProps__: {
                      type: 'rw',
                      streetAddress: 'rw',
                      locality: 'rw',
                      region: 'rw',
                      postalCode: 'rw',
                      countryName: 'rw',
                      pref: 'rw'
                     },

  classID : CONTACTADDRESS_CID,
  classInfo : XPCOMUtils.generateCI({classID: CONTACTADDRESS_CID,
                                     contractID: CONTACTADDRESS_CONTRACTID,
                                     classDescription: "ContactAddress",
                                     interfaces: [nsIContactAddress],
                                     flags: nsIClassInfo.DOM_OBJECT}),

  QueryInterface : XPCOMUtils.generateQI([nsIContactAddress])
}

//ContactField

const CONTACTFIELD_CONTRACTID = "@mozilla.org/contactField;1";
const CONTACTFIELD_CID        = Components.ID("{ad19a543-69e4-44f0-adfa-37c011556bc1}");
const nsIContactField         = Components.interfaces.nsIContactField;

function ContactField(aType, aValue, aPref) {
  this.type = sanitizeStringArray(aType);
  this.value = stringOrBust(aValue);
  this.pref = aPref;
};

ContactField.prototype = {
  __exposedProps__: {
                      type: 'rw',
                      value: 'rw',
                      pref: 'rw'
                     },

  classID : CONTACTFIELD_CID,
  classInfo : XPCOMUtils.generateCI({classID: CONTACTFIELD_CID,
                                     contractID: CONTACTFIELD_CONTRACTID,
                                     classDescription: "ContactField",
                                     interfaces: [nsIContactField],
                                     flags: nsIClassInfo.DOM_OBJECT}),

  QueryInterface : XPCOMUtils.generateQI([nsIContactField])
}

//ContactTelField

const CONTACTTELFIELD_CONTRACTID = "@mozilla.org/contactTelField;1";
const CONTACTTELFIELD_CID        = Components.ID("{4d42c5a9-ea5d-4102-80c3-40cc986367ca}");
const nsIContactTelField         = Components.interfaces.nsIContactTelField;

function ContactTelField(aType, aValue, aCarrier, aPref) {
  this.type = sanitizeStringArray(aType);
  this.value = stringOrBust(aValue);
  this.carrier = stringOrBust(aCarrier);
  this.pref = aPref;
};

ContactTelField.prototype = {
  __exposedProps__: {
                      type: 'rw',
                      value: 'rw',
                      carrier: 'rw',
                      pref: 'rw'
                     },

  classID : CONTACTTELFIELD_CID,
  classInfo : XPCOMUtils.generateCI({classID: CONTACTTELFIELD_CID,
                                     contractID: CONTACTTELFIELD_CONTRACTID,
                                     classDescription: "ContactTelField",
                                     interfaces: [nsIContactTelField],
                                     flags: nsIClassInfo.DOM_OBJECT}),

  QueryInterface : XPCOMUtils.generateQI([nsIContactTelField])
}

//ContactFindSortOptions

const CONTACTFINDSORTOPTIONS_CONTRACTID = "@mozilla.org/contactFindSortOptions;1"
const CONTACTFINDSORTOPTIONS_CID        = Components.ID("{0a5b1fab-70da-46dd-b902-619904d920c2}");
const nsIContactFindSortOptions         = Ci.nsIContactFindSortOptions;

function ContactFindSortOptions () { }

ContactFindSortOptions.prototype = {
  classID: CONTACTFINDSORTOPTIONS_CID,
  classInfo: XPCOMUtils.generateCI({classID: CONTACTFINDSORTOPTIONS_CID,
                                    contractID: CONTACTFINDSORTOPTIONS_CONTRACTID,
                                    classDescription: "ContactFindSortOptions",
                                    interfaces: [nsIContactFindSortOptions],
                                    flags: nsIClassInfo.DOM_OBJECT}),
  QueryInterface: XPCOMUtils.generateQI([nsIContactFindSortOptions])
};

//ContactFindOptions

const CONTACTFINDOPTIONS_CONTRACTID = "@mozilla.org/contactFindOptions;1";
const CONTACTFINDOPTIONS_CID        = Components.ID("{28ce07d0-45d9-4b7a-8843-521df4edd8bc}");
const nsIContactFindOptions         = Components.interfaces.nsIContactFindOptions;

function ContactFindOptions() { };

ContactFindOptions.prototype = {

  classID : CONTACTFINDOPTIONS_CID,
  classInfo : XPCOMUtils.generateCI({classID: CONTACTFINDOPTIONS_CID,
                                     contractID: CONTACTFINDOPTIONS_CONTRACTID,
                                     classDescription: "ContactFindOptions",
                                     interfaces: [nsIContactFindSortOptions,
                                                  nsIContactFindOptions],
                                     flags: nsIClassInfo.DOM_OBJECT}),

  QueryInterface : XPCOMUtils.generateQI([nsIContactFindSortOptions,
                                          nsIContactFindOptions])
}

//Contact

const CONTACT_CONTRACTID = "@mozilla.org/contact;1";
const CONTACT_CID        = Components.ID("{72a5ee28-81d8-4af8-90b3-ae935396cc66}");
const nsIDOMContact      = Components.interfaces.nsIDOMContact;

function checkBlobArray(aBlob) {
  if (Array.isArray(aBlob)) {
    for (let i = 0; i < aBlob.length; i++) {
      if (typeof aBlob != 'object') {
        return null;
      }
      if (!(aBlob[i] instanceof Components.interfaces.nsIDOMBlob)) {
        return null;
      }
    }
    return aBlob;
  }
  return null;
}

function isVanillaObj(aObj) {
  return Object.prototype.toString.call(aObj) == "[object Object]";
}

function validateArrayField(data, createCb) {
  if (data) {
    data = Array.isArray(data) ? data : [data];
    let filtered = [];
    for (let obj of data) {
      if (obj && isVanillaObj(obj)) {
        filtered.push(createCb(obj));
      }
    }
    return filtered;
  }
  return undefined;
}

function Contact() { };

Contact.prototype = {
  __exposedProps__: {
                      id: 'rw',
                      updated: 'rw',
                      published:  'rw',
                      name: 'rw',
                      honorificPrefix: 'rw',
                      givenName: 'rw',
                      additionalName: 'rw',
                      familyName: 'rw',
                      honorificSuffix: 'rw',
                      nickname: 'rw',
                      email: 'rw',
                      photo: 'rw',
                      url: 'rw',
                      category: 'rw',
                      adr: 'rw',
                      tel: 'rw',
                      org: 'rw',
                      jobTitle: 'rw',
                      bday: 'rw',
                      note: 'rw',
                      impp: 'rw',
                      anniversary: 'rw',
                      sex: 'rw',
                      genderIdentity: 'rw',
                      key: 'rw',
                     },

  set name(aName) {
    this._name = sanitizeStringArray(aName);
  },

  get name() {
    return this._name;
  },

  set honorificPrefix(aHonorificPrefix) {
    this._honorificPrefix = sanitizeStringArray(aHonorificPrefix);
  },

  get honorificPrefix() {
    return this._honorificPrefix;
  },

  set givenName(aGivenName) {
    this._givenName = sanitizeStringArray(aGivenName);
  },

  get givenName() {
    return this._givenName;
  },

  set additionalName(aAdditionalName) {
    this._additionalName = sanitizeStringArray(aAdditionalName);
  },

  get additionalName() {
    return this._additionalName;
  },

  set familyName(aFamilyName) {
    this._familyName = sanitizeStringArray(aFamilyName);
  },

  get familyName() {
    return this._familyName;
  },

  set honorificSuffix(aHonorificSuffix) {
    this._honorificSuffix = sanitizeStringArray(aHonorificSuffix);
  },

  get honorificSuffix() {
    return this._honorificSuffix;
  },

  set nickname(aNickname) {
    this._nickname = sanitizeStringArray(aNickname);
  },

  get nickname() {
    return this._nickname;
  },

  set photo(aPhoto) {
    this._photo = checkBlobArray(aPhoto);
  },

  get photo() {
    return this._photo;
  },

  set category(aCategory) {
    this._category = sanitizeStringArray(aCategory);
  },

  get category() {
    return this._category;
  },

  set email(aEmail) {
    this._email = validateArrayField(aEmail, function(email) {
      return new ContactField(email.type, email.value, email.pref);
    });
  },

  get email() {
    return this._email;
  },

  set adr(aAdr) {
    this._adr = validateArrayField(aAdr, function(adr) {
      return new ContactAddress(adr.type, adr.streetAddress, adr.locality,
                                adr.region, adr.postalCode, adr.countryName,
                                adr.pref);
    });
  },

  get adr() {
    return this._adr;
  },

  set tel(aTel) {
    this._tel = validateArrayField(aTel, function(tel) {
      return new ContactTelField(tel.type, tel.value, tel.carrier, tel.pref);
    });
  },

  get tel() {
    return this._tel;
  },

  set impp(aImpp) {
    this._impp = validateArrayField(aImpp, function(impp) {
      return new ContactField(impp.type, impp.value, impp.pref);
    });
  },

  get impp() {
    return this._impp;
  },

  set url(aUrl) {
    this._url = validateArrayField(aUrl, function(url) {
      return new ContactField(url.type, url.value, url.pref);
    });
  },

  get url() {
    return this._url;
  },

  set org(aOrg) {
    this._org = sanitizeStringArray(aOrg);
  },

  get org() {
    return this._org;
  },

  set jobTitle(aJobTitle) {
    this._jobTitle = sanitizeStringArray(aJobTitle);
  },

  get jobTitle() {
    return this._jobTitle;
  },

  set note(aNote) {
    this._note = sanitizeStringArray(aNote);
  },

  get note() {
    return this._note;
  },

  set bday(aBday) {
    if (aBday && aBday.constructor.name === "Date") {
      this._bday = aBday;
    } else if (typeof aBday === "string" || typeof aBday === "number") {
      this._bday = new Date(aBday);
    }
  },

  get bday() {
    return this._bday;
  },

  set anniversary(aAnniversary) {
    if (aAnniversary && aAnniversary.constructor.name === "Date") {
      this._anniversary = aAnniversary;
    } else if (typeof aAnniversary === "string" || typeof aAnniversary === "number") {
      this._anniversary = new Date(aAnniversary);
    }
  },

  get anniversary() {
    return this._anniversary;
  },

  set sex(aSex) {
    if (aSex !== "undefined") {
      this._sex = aSex;
    } else {
      this._sex = null;
    }
  },

  get sex() {
    return this._sex;
  },

  set genderIdentity(aGenderIdentity) {
    if (aGenderIdentity !== "undefined") {
      this._genderIdentity = aGenderIdentity;
    } else {
      this._genderIdentity = null;
    }
  },

  get genderIdentity() {
    return this._genderIdentity;
  },

  set key(aKey) {
    this._key = sanitizeStringArray(aKey);
  },

  get key() {
    return this._key;
  },

  init: function init(aProp) {
    this.name =            aProp.name;
    this.honorificPrefix = aProp.honorificPrefix;
    this.givenName =       aProp.givenName;
    this.additionalName =  aProp.additionalName;
    this.familyName =      aProp.familyName;
    this.honorificSuffix = aProp.honorificSuffix;
    this.nickname =        aProp.nickname;
    this.email =           aProp.email;
    this.photo =           aProp.photo;
    this.url =             aProp.url;
    this.category =        aProp.category;
    this.adr =             aProp.adr;
    this.tel =             aProp.tel;
    this.org =             aProp.org;
    this.jobTitle =        aProp.jobTitle;
    this.bday =            aProp.bday;
    this.note =            aProp.note;
    this.impp =            aProp.impp;
    this.anniversary =     aProp.anniversary;
    this.sex =             aProp.sex;
    this.genderIdentity =  aProp.genderIdentity;
    this.key =             aProp.key;
  },

  get published () {
    return this._published;
  },

  set published(aPublished) {
    this._published = aPublished;
  },

  get updated () {
    return this._updated;
  },

  set updated(aUpdated) {
    this._updated = aUpdated;
  },

  classID : CONTACT_CID,
  classInfo : XPCOMUtils.generateCI({classID: CONTACT_CID,
                                     contractID: CONTACT_CONTRACTID,
                                     classDescription: "Contact",
                                     interfaces: [nsIDOMContact, nsIContactProperties],
                                     flags: nsIClassInfo.DOM_OBJECT}),

  QueryInterface : XPCOMUtils.generateQI([nsIDOMContact, nsIContactProperties])
}

// ContactManager

const CONTACTMANAGER_CONTRACTID = "@mozilla.org/contactManager;1";
const CONTACTMANAGER_CID        = Components.ID("{8beb3a66-d70a-4111-b216-b8e995ad3aff}");
const nsIDOMContactManager      = Components.interfaces.nsIDOMContactManager;

function ContactManager()
{
  if (DEBUG) debug("Constructor");
}

ContactManager.prototype = {
  __proto__: DOMRequestIpcHelper.prototype,
  _oncontactchange: null,
  _cachedContacts: [] ,

  set oncontactchange(aCallback) {
    if (DEBUG) debug("set oncontactchange");
    let allowCallback = function() {
      if (!this._oncontactchange) {
        cpmm.sendAsyncMessage("Contacts:RegisterForMessages");
      }
      this._oncontactchange = aCallback;
    }.bind(this);
    let cancelCallback = function() {
      throw Components.results.NS_ERROR_FAILURE;
    }
    this.askPermission("listen", null, allowCallback, cancelCallback);
  },

  get oncontactchange() {
    return this._oncontactchange;
  },

  _setMetaData: function(aNewContact, aRecord) {
    aNewContact.id = aRecord.id;
    aNewContact.published = aRecord.published;
    aNewContact.updated = aRecord.updated;
  },

  _convertContact: function CM_convertContact(aContact) {
    let newContact = new Contact();
    newContact.init(aContact.properties);
    this._setMetaData(newContact, aContact);
    return newContact;
  },

  _convertContacts: function(aContacts) {
    let contacts = [];
    for (let i in aContacts) {
      contacts.push(this._convertContact(aContacts[i]));
    }
    return contacts;
  },

  _fireSuccessOrDone: function(aCursor, aResult) {
    if (aResult == null) {
      Services.DOMRequest.fireDone(aCursor);
    } else {
      Services.DOMRequest.fireSuccess(aCursor, aResult);
    }
  },

  _pushArray: function(aArr1, aArr2) {
    aArr1.push.apply(aArr1, aArr2);
  },

  receiveMessage: function(aMessage) {
    if (DEBUG) debug("receiveMessage: " + aMessage.name);
    let msg = aMessage.json;
    let contacts = msg.contacts;

    let req;
    switch (aMessage.name) {
      case "Contacts:Find:Return:OK":
        req = this.getRequest(msg.requestID);
        if (req) {
          let result = this._convertContacts(contacts);
          Services.DOMRequest.fireSuccess(req.request, result);
        } else {
          if (DEBUG) debug("no request stored!" + msg.requestID);
        }
        break;
      case "Contacts:GetAll:Next":
        let data = this.getRequest(msg.cursorId);
        if (!data) {
          break;
        }
        let result = contacts ? this._convertContacts(contacts) : [null];
        if (data.waitingForNext) {
          if (DEBUG) debug("cursor waiting for contact, sending");
          data.waitingForNext = false;
          let contact = result.shift();
          this._pushArray(data.cachedContacts, result);
          this.nextTick(this._fireSuccessOrDone.bind(this, data.cursor, contact));
          if (!contact) {
            this.removeRequest(msg.cursorId);
          }
        } else {
          if (DEBUG) debug("cursor not waiting, saving");
          this._pushArray(data.cachedContacts, result);
        }
        break;
      case "Contact:Save:Return:OK":
        // If a cached contact was saved and a new contact ID was returned, update the contact's ID
        if (this._cachedContacts[msg.requestID]) {
          if (msg.contactID) {
            this._cachedContacts[msg.requestID].id = msg.contactID;
          }
          delete this._cachedContacts[msg.requestID];
        }
      case "Contacts:Clear:Return:OK":
      case "Contact:Remove:Return:OK":
        req = this.getRequest(msg.requestID);
        if (req)
          Services.DOMRequest.fireSuccess(req.request, null);
        break;
      case "Contacts:Find:Return:KO":
      case "Contact:Save:Return:KO":
      case "Contact:Remove:Return:KO":
      case "Contacts:Clear:Return:KO":
      case "Contacts:GetRevision:Return:KO":
      case "Contacts:Count:Return:KO":
        req = this.getRequest(msg.requestID);
        if (req) {
          if (req.request) {
            req = req.request;
          }
          Services.DOMRequest.fireError(req, msg.errorMsg);
        }
        break;
      case "Contacts:GetAll:Return:KO":
        req = this.getRequest(msg.requestID);
        if (req) {
          Services.DOMRequest.fireError(req.cursor, msg.errorMsg);
        }
        break;
      case "PermissionPromptHelper:AskPermission:OK":
        if (DEBUG) debug("id: " + msg.requestID);
        req = this.getRequest(msg.requestID);
        if (!req) {
          break;
        }

        if (msg.result == Ci.nsIPermissionManager.ALLOW_ACTION) {
          req.allow();
        } else {
          req.cancel();
        }
        break;
      case "Contact:Changed":
        // Fire oncontactchange event
        if (DEBUG) debug("Contacts:ContactChanged: " + msg.contactID + ", " + msg.reason);
        if (this._oncontactchange) {
          let event = new this._window.MozContactChangeEvent("contactchanged", {
            contactID: msg.contactID,
            reason: msg.reason
          });
          this._oncontactchange.handleEvent(event);
        }
        break;
      case "Contacts:Revision":
        if (DEBUG) debug("new revision: " + msg.revision);
        req = this.getRequest(msg.requestID);
        if (req) {
          Services.DOMRequest.fireSuccess(req.request, msg.revision);
        }
        break;
      case "Contacts:Count":
        if (DEBUG) debug("count: " + msg.count);
        req = this.getRequest(msg.requestID);
        if (req) {
          Services.DOMRequest.fireSuccess(req.request, msg.count);
        }
        break;
      default:
        if (DEBUG) debug("Wrong message: " + aMessage.name);
    }
    this.removeRequest(msg.requestID);
  },

  askPermission: function (aAccess, aRequest, aAllowCallback, aCancelCallback) {
    if (DEBUG) debug("askPermission for contacts");
    let access;
    switch(aAccess) {
      case "create":
        access = "create";
        break;
      case "update":
      case "remove":
        access = "write";
        break;
      case "find":
      case "listen":
      case "revision":
      case "count":
        access = "read";
        break;
      default:
        access = "unknown";
      }

    // Shortcut for ALLOW_ACTION so we avoid a parent roundtrip
    let type = "contacts-" + access;
    let permValue =
      pm.testExactPermissionFromPrincipal(this._window.document.nodePrincipal, type);
    if (permValue == Ci.nsIPermissionManager.ALLOW_ACTION) {
      aAllowCallback();
      return;
    }

    let requestID = this.getRequestId({
      request: aRequest,
      allow: function() {
        aAllowCallback();
      }.bind(this),
      cancel : function() {
        if (aCancelCallback) {
          aCancelCallback()
        } else if (aRequest) {
          Services.DOMRequest.fireError(aRequest, "Not Allowed");
        }
      }.bind(this)
    });

    let principal = this._window.document.nodePrincipal;
    cpmm.sendAsyncMessage("PermissionPromptHelper:AskPermission", {
      type: "contacts",
      access: access,
      requestID: requestID,
      origin: principal.origin,
      appID: principal.appId,
      browserFlag: principal.isInBrowserElement,
      windowID: this._window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID
    });
  },

  save: function save(aContact) {
    if (DEBUG) debug("save: " + JSON.stringify(aContact) + " :" + aContact.id);
    let newContact = {};
    newContact.properties = {
      name:            [],
      honorificPrefix: [],
      givenName:       [],
      additionalName:  [],
      familyName:      [],
      honorificSuffix: [],
      nickname:        [],
      email:           [],
      photo:           [],
      url:             [],
      category:        [],
      adr:             [],
      tel:             [],
      org:             [],
      jobTitle:        [],
      bday:            null,
      note:            [],
      impp:            [],
      anniversary:     null,
      sex:             null,
      genderIdentity:  null,
      key:             [],
    };
    for (let field in newContact.properties) {
      newContact.properties[field] = aContact[field];
    }
    let request = this.createRequest();
    let requestID = this.getRequestId({request: request, reason: reason});

    let reason;
    if (aContact.id == "undefined") {
      // for example {25c00f01-90e5-c545-b4d4-21E2ddbab9e0} becomes
      // 25c00f0190e5c545b4d421E2ddbab9e0
      aContact.id = this._getRandomId().replace('-', '', 'g').replace('{', '').replace('}', '');
      // Cache the contact so that its ID may be updated later if necessary
      this._cachedContacts[requestID] = aContact;
      reason = "create";
    } else {
      reason = "update";
    }

    this._setMetaData(newContact, aContact);
    if (DEBUG) debug("send: " + JSON.stringify(newContact));
    let options = { contact: newContact, reason: reason };
    let allowCallback = function() {
      cpmm.sendAsyncMessage("Contact:Save", {requestID: requestID, options: options});
    }.bind(this)
    this.askPermission(reason, request, allowCallback);
    return request;
  },

  find: function(aOptions) {
    if (DEBUG) debug("find! " + JSON.stringify(aOptions));
    let request = this.createRequest();
    let options = { findOptions: aOptions };
    let allowCallback = function() {
      cpmm.sendAsyncMessage("Contacts:Find", {requestID: this.getRequestId({request: request, reason: "find"}), options: options});
    }.bind(this)
    this.askPermission("find", request, allowCallback);
    return request;
  },

  createCursor: function CM_createCursor(aRequest) {
    let data = {
      cursor: Services.DOMRequest.createCursor(this._window, function() {
        this.handleContinue(id);
      }.bind(this)),
      cachedContacts: [],
      waitingForNext: true,
    };
    let id = this.getRequestId(data);
    if (DEBUG) debug("saved cursor id: " + id);
    return [id, data.cursor];
  },

  getAll: function CM_getAll(aOptions) {
    if (DEBUG) debug("getAll: " + JSON.stringify(aOptions));
    let [cursorId, cursor] = this.createCursor();
    let allowCallback = function() {
      cpmm.sendAsyncMessage("Contacts:GetAll", {
        cursorId: cursorId, findOptions: aOptions});
    }.bind(this);
    this.askPermission("find", cursor, allowCallback);
    return cursor;
  },

  nextTick: function nextTick(aCallback) {
    Services.tm.currentThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL);
  },

  handleContinue: function CM_handleContinue(aCursorId) {
    if (DEBUG) debug("handleContinue: " + aCursorId);
    let data = this.getRequest(aCursorId);
    if (data.cachedContacts.length > 0) {
      if (DEBUG) debug("contact in cache");
      let contact = data.cachedContacts.shift();
      this.nextTick(this._fireSuccessOrDone.bind(this, data.cursor, contact));
      if (!contact) {
        this.removeRequest(aCursorId);
      } else if (data.cachedContacts.length === CONTACTS_SENDMORE_MINIMUM) {
        cpmm.sendAsyncMessage("Contacts:GetAll:SendNow", { cursorId: aCursorId });
      }
    } else {
      if (DEBUG) debug("waiting for contact");
      data.waitingForNext = true;
    }
  },

  remove: function removeContact(aRecord) {
    let request = this.createRequest();
    if (!aRecord || !aRecord.id) {
      Services.DOMRequest.fireErrorAsync(request, true);
      return request;
    }

    let options = { id: aRecord.id };
    let allowCallback = function() {
      cpmm.sendAsyncMessage("Contact:Remove", {requestID: this.getRequestId({request: request, reason: "remove"}), options: options});
    }.bind(this)
    this.askPermission("remove", request, allowCallback);
    return request;
  },

  clear: function() {
    if (DEBUG) debug("clear");
    let request;
    request = this.createRequest();
    let options = {};
    let allowCallback = function() {
      cpmm.sendAsyncMessage("Contacts:Clear", {requestID: this.getRequestId({request: request, reason: "remove"}), options: options});
    }.bind(this)
    this.askPermission("remove", request, allowCallback);
    return request;
  },

  getRevision: function() {
    let request = this.createRequest();

    let allowCallback = function() {
      cpmm.sendAsyncMessage("Contacts:GetRevision", {
        requestID: this.getRequestId({ request: request })
      });
    }.bind(this);

    let cancelCallback = function() {
      Services.DOMRequest.fireError(request);
    };

    this.askPermission("revision", request, allowCallback, cancelCallback);
    return request;
  },

  getCount: function() {
    let request = this.createRequest();

    let allowCallback = function() {
      cpmm.sendAsyncMessage("Contacts:GetCount", {
        requestID: this.getRequestId({ request: request })
      });
    }.bind(this);

    let cancelCallback = function() {
      Services.DOMRequest.fireError(request);
    };

    this.askPermission("count", request, allowCallback, cancelCallback);
    return request;
  },

  init: function(aWindow) {
    this.initDOMRequestHelper(aWindow, ["Contacts:Find:Return:OK", "Contacts:Find:Return:KO",
                              "Contacts:Clear:Return:OK", "Contacts:Clear:Return:KO",
                              "Contact:Save:Return:OK", "Contact:Save:Return:KO",
                              "Contact:Remove:Return:OK", "Contact:Remove:Return:KO",
                              "Contact:Changed",
                              "PermissionPromptHelper:AskPermission:OK",
                              "Contacts:GetAll:Next", "Contacts:GetAll:Return:KO",
                              "Contacts:Count",
                              "Contacts:Revision", "Contacts:GetRevision:Return:KO",]);
  },

  // Called from DOMRequestIpcHelper
  uninit: function uninit() {
    if (DEBUG) debug("uninit call");
    if (this._oncontactchange)
      this._oncontactchange = null;
  },

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

  classInfo : XPCOMUtils.generateCI({classID: CONTACTMANAGER_CID,
                                     contractID: CONTACTMANAGER_CONTRACTID,
                                     classDescription: "ContactManager",
                                     interfaces: [nsIDOMContactManager],
                                     flags: nsIClassInfo.DOM_OBJECT})
}

this.NSGetFactory = XPCOMUtils.generateNSGetFactory(
                       [Contact, ContactManager, ContactProperties, ContactAddress, ContactField, ContactTelField, ContactFindSortOptions, ContactFindOptions])
