/* 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 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/SettingsRequestManager.jsm');

/* static functions */
var DEBUG = false;
var VERBOSE = false;

try {
  DEBUG   =
    Services.prefs.getBoolPref("dom.mozSettings.SettingsService.debug.enabled");
  VERBOSE =
    Services.prefs.getBoolPref("dom.mozSettings.SettingsService.verbose.enabled");
} catch (ex) { }

function debug(s) {
  dump("-*- SettingsService: " + s + "\n");
}

XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
                                   "@mozilla.org/uuid-generator;1",
                                   "nsIUUIDGenerator");
XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
                                   "@mozilla.org/childprocessmessagemanager;1",
                                   "nsIMessageSender");
XPCOMUtils.defineLazyServiceGetter(this, "mrm",
                                   "@mozilla.org/memory-reporter-manager;1",
                                   "nsIMemoryReporterManager");

const nsIClassInfo                   = Ci.nsIClassInfo;
const kXpcomShutdownObserverTopic    = "xpcom-shutdown";

const SETTINGSSERVICELOCK_CONTRACTID = "@mozilla.org/settingsServiceLock;1";
const SETTINGSSERVICELOCK_CID        = Components.ID("{d7a395a0-e292-11e1-834e-1761d57f5f99}");
const nsISettingsServiceLock         = Ci.nsISettingsServiceLock;

function makeSettingsServiceRequest(aCallback, aName, aValue) {
  return {
    callback: aCallback,
    name: aName,
    value: aValue
  };
};

const kLockListeners = ["Settings:Get:OK", "Settings:Get:KO",
                        "Settings:Clear:OK", "Settings:Clear:KO",
                        "Settings:Set:OK", "Settings:Set:KO",
                        "Settings:Finalize:OK", "Settings:Finalize:KO"];

function SettingsServiceLock(aSettingsService, aTransactionCallback) {
  if (VERBOSE) debug("settingsServiceLock constr!");
  this._open = true;
  this._settingsService = aSettingsService;
  this._id = uuidgen.generateUUID().toString();
  this._transactionCallback = aTransactionCallback;
  this._requests = {};
  let closeHelper = function() {
    if (VERBOSE) debug("closing lock " + this._id);
    this._open = false;
    this.runOrFinalizeQueries();
  }.bind(this);

  this.addListeners();

  let createLockPayload = {
    lockID: this._id,
    isServiceLock: true,
    windowID: undefined,
    lockStack: (new Error).stack
  };

  this.returnMessage("Settings:CreateLock", createLockPayload);
  Services.tm.currentThread.dispatch(closeHelper, Ci.nsIThread.DISPATCH_NORMAL);
}

SettingsServiceLock.prototype = {
  get closed() {
    return !this._open;
  },

  addListeners: function() {
    for (let msg of kLockListeners) {
      cpmm.addMessageListener(msg, this);
    }
  },

  removeListeners: function() {
    for (let msg of kLockListeners) {
      cpmm.removeMessageListener(msg, this);
    }
  },

  returnMessage: function(aMessage, aData) {
    SettingsRequestManager.receiveMessage({
      name: aMessage,
      data: aData,
      target: undefined,
      principal: Services.scriptSecurityManager.getSystemPrincipal()
    });
  },

  runOrFinalizeQueries: function() {
    if (!this._requests || Object.keys(this._requests).length == 0) {
      this.returnMessage("Settings:Finalize", {lockID: this._id});
    } else {
      this.returnMessage("Settings:Run", {lockID: this._id});
    }
  },

  receiveMessage: function(aMessage) {

    let msg = aMessage.data;
    // SettingsRequestManager broadcasts changes to all locks in the child. If
    // our lock isn't being addressed, just return.
    if(msg.lockID != this._id) {
      return;
    }
    if (VERBOSE) debug("receiveMessage (" + this._id + "): " + aMessage.name);
    // Finalizing a transaction does not return a request ID since we are
    // supposed to fire callbacks.
    if (!msg.requestID) {
      switch (aMessage.name) {
        case "Settings:Finalize:OK":
          if (VERBOSE) debug("Lock finalize ok!");
          this.callTransactionHandle();
          break;
        case "Settings:Finalize:KO":
          if (DEBUG) debug("Lock finalize failed!");
          this.callAbort();
          break;
        default:
          if (DEBUG) debug("Message type " + aMessage.name + " is missing a requestID");
      }

      this._settingsService.unregisterLock(this._id);
      return;
    }

    let req = this._requests[msg.requestID];
    if (!req) {
      if (DEBUG) debug("Matching request not found.");
      return;
    }
    delete this._requests[msg.requestID];
    switch (aMessage.name) {
      case "Settings:Get:OK":
        this._open = true;
        let settings_names = Object.keys(msg.settings);
        if (settings_names.length > 0) {
          let name = settings_names[0];
          if (DEBUG && settings_names.length > 1) {
            debug("Warning: overloaded setting:" + name);
          }
          let result = msg.settings[name];
          this.callHandle(req.callback, name, result);
        } else {
          this.callHandle(req.callback, req.name, null);
        }
        this._open = false;
        break;
      case "Settings:Set:OK":
        this._open = true;
        // We don't pass values back from sets in SettingsManager...
        this.callHandle(req.callback, req.name, req.value);
        this._open = false;
        break;
      case "Settings:Get:KO":
      case "Settings:Set:KO":
        if (DEBUG) debug("error:" + msg.errorMsg);
        this.callError(req.callback, msg.error);
        break;
      default:
        if (DEBUG) debug("Wrong message: " + aMessage.name);
    }
    this.runOrFinalizeQueries();
  },

  get: function get(aName, aCallback) {
    if (VERBOSE) debug("get (" + this._id + "): " + aName);
    if (!this._open) {
      if (DEBUG) debug("Settings lock not open!\n");
      throw Components.results.NS_ERROR_ABORT;
    }
    let reqID = uuidgen.generateUUID().toString();
    this._requests[reqID] = makeSettingsServiceRequest(aCallback, aName);
    this.returnMessage("Settings:Get", {requestID: reqID,
                                        lockID: this._id,
                                        name: aName});
  },

  set: function set(aName, aValue, aCallback) {
    if (VERBOSE) debug("set: " + aName + " " + aValue);
    if (!this._open) {
      throw "Settings lock not open";
    }
    let reqID = uuidgen.generateUUID().toString();
    this._requests[reqID] = makeSettingsServiceRequest(aCallback, aName, aValue);
    let settings = {};
    settings[aName] = aValue;
    this.returnMessage("Settings:Set", {requestID: reqID,
                                        lockID: this._id,
                                        settings: settings});
  },

  callHandle: function callHandle(aCallback, aName, aValue) {
    try {
        aCallback && aCallback.handle ? aCallback.handle(aName, aValue) : null;
    } catch (e) {
      if (DEBUG) debug("settings 'handle' for " + aName + " callback threw an exception, dropping: " + e + "\n");
    }
  },

  callAbort: function callAbort(aCallback, aMessage) {
    try {
      aCallback && aCallback.handleAbort ? aCallback.handleAbort(aMessage) : null;
    } catch (e) {
      if (DEBUG) debug("settings 'abort' callback threw an exception, dropping: " + e + "\n");
    }
  },

  callError: function callError(aCallback, aMessage) {
    try {
      aCallback && aCallback.handleError ? aCallback.handleError(aMessage) : null;
    } catch (e) {
      if (DEBUG) debug("settings 'error' callback threw an exception, dropping: " + e + "\n");
    }
  },

  callTransactionHandle: function callTransactionHandle() {
    try {
      this._transactionCallback && this._transactionCallback.handle ? this._transactionCallback.handle() : null;
    } catch (e) {
      if (DEBUG) debug("settings 'Transaction handle' callback threw an exception, dropping: " + e + "\n");
    }
  },

  classID : SETTINGSSERVICELOCK_CID,
  QueryInterface : XPCOMUtils.generateQI([nsISettingsServiceLock])
};

const SETTINGSSERVICE_CID        = Components.ID("{f656f0c0-f776-11e1-a21f-0800200c9a66}");

function SettingsService()
{
  if (VERBOSE) debug("settingsService Constructor");
  this._locks = [];
  this._serviceLocks = {};
  this._createdLocks = 0;
  this._unregisteredLocks = 0;
  this.init();
}

SettingsService.prototype = {

  init: function() {
    Services.obs.addObserver(this, kXpcomShutdownObserverTopic, false);
    mrm.registerStrongReporter(this);
  },

  uninit: function() {
    Services.obs.removeObserver(this, kXpcomShutdownObserverTopic);
    mrm.unregisterStrongReporter(this);
  },

  observe: function(aSubject, aTopic, aData) {
    if (VERBOSE) debug("observe: " + aTopic);
    if (aTopic === kXpcomShutdownObserverTopic) {
      this.uninit();
    }
  },

  receiveMessage: function(aMessage) {
    if (VERBOSE) debug("Entering receiveMessage");

    let lockID = aMessage.data.lockID;
    if (!lockID) {
      if (DEBUG) debug("No lock ID");
      return;
    }

    if (!(lockID in this._serviceLocks)) {
      if (DEBUG) debug("Received message for lock " + lockID + " but no lock");
      return;
    }

    if (VERBOSE) debug("Delivering message");
    this._serviceLocks[lockID].receiveMessage(aMessage);
  },

  createLock: function createLock(aCallback) {
    if (VERBOSE) debug("Calling createLock");
    var lock = new SettingsServiceLock(this, aCallback);
    if (VERBOSE) debug("Created lock " + lock._id);
    this.registerLock(lock);
    return lock;
  },

  registerLock: function(aLock) {
    if (VERBOSE) debug("Registering lock " + aLock._id);
    this._locks.push(aLock._id);
    this._serviceLocks[aLock._id] = aLock;
    this._createdLocks++;
  },

  unregisterLock: function(aLockID) {
    let lock_index = this._locks.indexOf(aLockID);
    if (lock_index != -1) {
      if (VERBOSE) debug("Unregistering lock " + aLockID);
      this._locks.splice(lock_index, 1);
      this._serviceLocks[aLockID].removeListeners();
      this._serviceLocks[aLockID] = null;
      delete this._serviceLocks[aLockID];
      this._unregisteredLocks++;
    }
  },

  collectReports: function(aCallback, aData, aAnonymize) {
    aCallback.callback("",
                       "settings-service-locks/alive",
                       Ci.nsIMemoryReporter.KIND_OTHER,
                       Ci.nsIMemoryReporter.UNITS_COUNT,
                       this._locks.length,
                       "The number of service locks that are currently alives.",
                       aData);

    aCallback.callback("",
                       "settings-service-locks/created",
                       Ci.nsIMemoryReporter.KIND_OTHER,
                       Ci.nsIMemoryReporter.UNITS_COUNT,
                       this._createdLocks,
                       "The number of service locks that were created.",
                       aData);

    aCallback.callback("",
                       "settings-service-locks/deleted",
                       Ci.nsIMemoryReporter.KIND_OTHER,
                       Ci.nsIMemoryReporter.UNITS_COUNT,
                       this._unregisteredLocks,
                       "The number of service locks that were deleted.",
                       aData);
  },

  classID : SETTINGSSERVICE_CID,
  QueryInterface : XPCOMUtils.generateQI([Ci.nsISettingsService,
                                          Ci.nsIObserver,
                                          Ci.nsIMemoryReporter])
};

this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SettingsService, SettingsServiceLock]);