// -*- mode: js; js-indent-level: 2; indent-tabs-mode: nil -*-
///////////////////////// Component-global "Constants" /////////////////////////

var DESCRIPTION = "UW_GreasemonkeyService";
var CONTRACTID = "@unity_webapps.mozdev.org/unity_webapps-service;1";
var CLASSID = Components.ID("{2a0b5340-2714-11e1-bfc2-0800200c9a66}");

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

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

function UnityWebapps() {
  try {
    this.lib = ctypes.open("libunity-webapps.so.0");
  } catch (e) {
    try {
      this.lib = ctypes.open("/usr/local/lib/libunity-webapps.so.0");
    } catch (e) {
      this.lib = ctypes.open("/usr/lib/libunity-webapps.so.0");
    }
  }
  this.permissions_get_domain_dontask =
    this.lib.declare("unity_webapps_permissions_get_domain_dontask",
                     ctypes.default_abi,
                     ctypes.int32_t,ctypes.char.ptr);
  this.permissions_dontask_domain =
    this.lib.declare("unity_webapps_permissions_dontask_domain",
                     ctypes.default_abi,
                     ctypes.void_t,ctypes.char.ptr);
  this.permissions_is_integration_allowed =
    this.lib.declare("unity_webapps_permissions_is_integration_allowed",
                     ctypes.default_abi,
                     ctypes.int32_t);
  this.permissions_allow_domain =
    this.lib.declare("unity_webapps_permissions_allow_domain",
   		     ctypes.default_abi,
		     ctypes.void_t,
		     ctypes.char.ptr);
	
}

function UnityWebappsRepository() {
  try {
    this.lib = ctypes.open("libunity-webapps-repository.so.0");
  } catch (e) {
    try {
      this.lib = ctypes.open("/usr/local/lib/libunity-webapps-repository.so.0");
    } catch (e) {
      this.lib = ctypes.open("libunity-webapps-repository.so.0");
    }
  }

  this.ApplicationRepositoryInstallCallbackType =
    ctypes.FunctionType(ctypes.default_abi, ctypes.void_t, [ctypes.voidptr_t, ctypes.char.ptr, ctypes.int32_t, ctypes.voidptr_t,]);
  this.application_repository_new_default =
    this.lib.declare("unity_webapps_application_repository_new_default",
		     ctypes.default_abi, ctypes.voidptr_t);
  this.application_repository_get_resolved_application_status =
    this.lib.declare("unity_webapps_application_repository_get_resolved_application_status",
		     ctypes.default_abi, ctypes.int32_t, ctypes.voidptr_t, ctypes.char.ptr);
  this.application_repository_get_resolved_application_name =
    this.lib.declare("unity_webapps_application_repository_get_resolved_application_name",
		     ctypes.default_abi, ctypes.char.ptr, ctypes.voidptr_t, ctypes.char.ptr);
  this.application_repository_get_resolved_application_domain =
    this.lib.declare("unity_webapps_application_repository_get_resolved_application_domain",
		     ctypes.default_abi, ctypes.char.ptr, ctypes.voidptr_t, ctypes.char.ptr);
  this.application_repository_get_userscript_contents =
    this.lib.declare("unity_webapps_application_repository_get_userscript_contents",
		     ctypes.default_abi, ctypes.char.ptr, ctypes.voidptr_t, ctypes.char.ptr);
  this.application_repository_install_application =
    this.lib.declare("unity_webapps_application_repository_install_application",
		     ctypes.default_abi, ctypes.void_t, ctypes.voidptr_t, ctypes.char.ptr, this.ApplicationRepositoryInstallCallbackType.ptr, ctypes.voidptr_t);
  this.application_repository_prepare =
    this.lib.declare("unity_webapps_application_repository_prepare",
		     ctypes.default_abi, ctypes.int32_t, ctypes.voidptr_t);
  this.application_repository_resolve_url_as_json =
    this.lib.declare("unity_webapps_application_repository_resolve_url_as_json",
		     ctypes.default_abi, ctypes.char.ptr, ctypes.voidptr_t, ctypes.char.ptr);

  var glib = ctypes.open("libglib-2.0.so.0");

  this.g_free = glib.declare("g_free",
                             ctypes.default_abi,
                             ctypes.void_t, ctypes.voidptr_t);


  this.APPLICATION_STATUS_INSTALLED = 1;
  this.APPLICATION_STATUS_AVAILABLE = 0;
}

function getScriptSource(uwr, repo, name) {
  var ptr = uwr.application_repository_get_userscript_contents(repo, name);
  var src = ptr.readString();

  uwr.g_free(ptr);

  return src;
}

// \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ //

function UW_ScriptLogger(scriptName) {
  var namespace = 'uw/';

  this.prefix = [namespace, scriptName, ": "].join("");
}

UW_ScriptLogger.prototype.consoleService = Components
    .classes["@mozilla.org/consoleservice;1"]
    .getService(Components.interfaces.nsIConsoleService);

UW_ScriptLogger.prototype.log = function(message) {
  this.consoleService.logStringMessage(this.prefix + '\n' + message);
};

// \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ //

function UW_console(scriptName) {
  // based on http://www.getfirebug.com/firebug/firebugx.js
  var names = [
    "debug", "warn", "error", "info", "assert", "dir", "dirxml",
    "group", "groupEnd", "time", "timeEnd", "count", "trace", "profile",
    "profileEnd"
  ];

  for (var i=0, name; name=names[i]; i++) {
    this[name] = function() {};
  }

  // Important to use this private variable so that user scripts can't make
  // this call something else by redefining <this> or <logger>.
  var logger = new UW_ScriptLogger(scriptName);
  this.log = function() {
    logger.log(
      Array.prototype.slice.apply(arguments).join("\n")
    );
  };
}

UW_console.prototype.log = function() {
};

// \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ // \\ //

var gmRunScriptFilename = "resource://unity_webapps/runScript.js";
var gExtensionPath = (function() {
  try {
  // Turn the file:/// URL into an nsIFile ...
  var ioService = Components.classes["@mozilla.org/network/io-service;1"]
      .getService(Components.interfaces.nsIIOService);
  var uri = ioService.newURI(Components.stack.filename, null, null);
  var file = uri.QueryInterface(Components.interfaces.nsIFileURL).file;
  // ... to find the containing directory.
  var dir = file.parent.parent;
  // Then get the URL back for that path.
  return ioService.newFileURI(dir).spec;
  } catch (e) { dump(e+'\n'+uneval(e)+'\n\n'); return 'x'; }
})();

// Only a particular set of strings are allowed.  See: http://goo.gl/ex2LJ
var gMaxJSVersion = "ECMAv5";

var gMenuCommands = [];
var gStartupHasRun = false;

var gFileProtocolHandler = Components
    .classes["@mozilla.org/network/protocol;1?name=file"]
    .getService(Ci.nsIFileProtocolHandler);
var gTmpDir = Components.classes["@mozilla.org/file/directory_service;1"]
    .getService(Components.interfaces.nsIProperties)
    .get("TmpD", Components.interfaces.nsILocalFile);

// Examines the stack to determine if an API should be callable.
function UW_apiLeakCheck(apiName) {
  var stack = Components.stack;

  do {
    // Valid locations for GM API calls are:
    //  * Greasemonkey modules.
    //  * All of chrome.  (In the script update case, chrome will list values.)
    //  * Greasemonkey extension by path. (FF 3 does this instead of the above.)
    // Anything else on the stack and we will reject the API, to make sure that
    // the content window (whose path would be e.g. http://...) has no access.
    if (2 == stack.language
        && stack.filename.substr(0, 24) !== 'resource://unity_webapps/'
        && stack.filename.substr(0, 9) !== 'chrome://'
        && stack.filename.substr(0, gExtensionPath.length) !== gExtensionPath
        ) {
      UW_util.logError(new Error("Greasemonkey access violation: " +
          "unsafeWindow cannot call " + apiName + "."));
      return false;
    }

    stack = stack.caller;
  } while (stack);

  return true;
}

function createSandbox(aScriptName, aContentWin, aChromeWin, aFirebugConsole, aUrl
) {
  var unsafeWin = aContentWin.wrappedJSObject;
  var sandbox = new Components.utils.Sandbox(aContentWin, {sandboxPrototype: aContentWin});

  sandbox.unsafeWindow = unsafeWin;
  sandbox.XPathResult = Ci.nsIDOMXPathResult;

  sandbox.console = aFirebugConsole ? aFirebugConsole : new UW_console(aScriptName);
  sandbox.external = unsafeWin.external;

  return sandbox;
}

function findError(script, lineNumber) {
  var start = 0;
  var end = 1;

  for (var i = 0; i < script.offsets.length; i++) {
    end = script.offsets[i];
    if (lineNumber <= end) {
      return {
        uri: script.requires[i].fileURL,
        lineNumber: (lineNumber - start)
      };
    }
    start = end;
  }

  return {
    uri: script.fileURL,
    lineNumber: (lineNumber - end)
  };
}

function getFirebugConsole(wrappedContentWin, chromeWin) {
  try {
    return chromeWin.Firebug
        && chromeWin.Firebug.getConsoleByGlobal(wrappedContentWin)
        || null;
  } catch (e) {
    dump('Greasemonkey: Failure Firebug console:\n' + uneval(e) + '\n');
    return null;
  }
}

function isTempScript(uri) {
  if (uri.scheme != "file") return false;
  var file = gFileProtocolHandler.getFileFromURLSpec(uri.spec);
  return gTmpDir.contains(file, true);
}

function runScriptInSandbox(code, sandbox) {
  try {
    UW_runScript(code, sandbox, gMaxJSVersion);
  } catch (e) { // catches errors while running the script code
    try {
      if (e && "return not in function" == e.message) {
        // Means this script depends on the function enclosure.
        return false;
      }

      // Most errors seem to have a ".fileName", but rarely they're in
      // ".filename" instead.
      var fileName = e.fileName || e.filename;

      if (fileName == gmRunScriptFilename) {
        /* TODO: port this
        var err = findError(script, e.lineNumber);
        UW_util.logError(
             e, // error obj
             0, // 0 = error (1 = warning)
             err.uri, err.lineNumber);*/
        UW_util.logError(e);
      } else {
        UW_util.logError(e);
      }
    } catch (e) {
      // Do not raise (this would stop all scripts), log.
      UW_util.logError(e);
    }
  }
  return true; // did not need a (function() {...})() enclosure.
}

function startup() {
  if (gStartupHasRun) return;
  gStartupHasRun = true;

  Cu.import(gmRunScriptFilename);
  Cu.import("resource://unity_webapps/utils.js");  // At top = fail in FF3.

  var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
      .getService(Ci.mozIJSSubScriptLoader);
  loader.loadSubScript("chrome://global/content/XPCNativeWrapper.js");
}

function suggestAppInstall(uw, uwr, repo, chromeWindow, contentWindow, appId, url) {
  let appDomain = uwr.application_repository_get_resolved_application_domain (repo, appId);
  if (!appDomain.isNull() && uw.permissions_get_domain_dontask(appDomain.readString()))
    return;
  if (repo.install_request_stamps[appId] !== undefined) {
    return;
  }
  repo.install_request_stamps[appId] = true;

  try {
    Cu.import("resource://unity/l10n.js");

    let appName = uwr.application_repository_get_resolved_application_name (repo, appId);


    var promptMsg = l10n.formatMessage('webappsGreasemonkey.installPrompt', [appName.readString()]);
    var dontaskMsg = l10n.formatMessage('prompt.dontask');
    var installMsg = l10n.formatMessage('webappsGreasemonkey.install');

    Cu.unload("resource://unity/l10n.js");
  } catch (x) {return;}

  function onInstall(_repo, name, status, data) {
    delete repo.saved_install_callbacks[appId];

    if (status == uwr.APPLICATION_STATUS_INSTALLED) {
      if (!appDomain.isNull()) {
        uw.permissions_allow_domain (appDomain.readString());
      }
      var scripts = [];
      var src = getScriptSource(uwr, repo, name);
      scripts.push({ name: name, content: src });
      UW_util.getService().injectScripts(scripts, url, contentWindow, chromeWindow);
    }
  }
  onInstall = uwr.ApplicationRepositoryInstallCallbackType.ptr(onInstall);

  function installApp() {
    if (repo.saved_install_callbacks[appId] === undefined) {
      repo.saved_install_callbacks[appId] = onInstall;

      uwr.application_repository_install_application(repo, appId, onInstall, null);
    }
  }

  function addToIgnoreList() {
    if (!appDomain.isNull()) {
      uw.permissions_dontask_domain(appDomain.readString());
    }
  }
    
  chromeWindow.PopupNotifications.show(chromeWindow.gBrowser.getBrowserForDocument(contentWindow.document),
				       "unity-permission-popup",
				       promptMsg,
				       null,
				       {
					 label: installMsg,
					 accessKey: "I",
					 callback: installApp
				       },
				       [
					 {
					   label: dontaskMsg,
					   accessKey: "D",
					   callback: addToIgnoreList
					 }
				       ]);
}

/////////////////////////////////// Service ////////////////////////////////////

function service() {
  this.wrappedJSObject = this;
    
  // A bit of a hack
  // We need to ensure the storage subsystem is initialized before we
  // use sqlite from libunity-webapps-repository.so otherwise
  // sqlite3_config may fail in firefox.
  // https://bugs.launchpad.net/ubuntu/quantal/+source/webapps-greasemonkey/+bug/1058209
  // Would be nice to remove as it has an impact on startup time.
  let sadness = Services.storage;
    

  this._uw = new UnityWebapps();
  this._uwr = new UnityWebappsRepository();
  this._repo = this._uwr.application_repository_new_default();
  this._repo.saved_install_callbacks = {};
  this._repo.install_request_stamps = {};
  this._uwr.application_repository_prepare(this._repo);
}

////////////////////////////////// Constants ///////////////////////////////////

service.prototype.classDescription = DESCRIPTION;
service.prototype.classID = CLASSID;
service.prototype.contractID = CONTRACTID;
service.prototype._xpcom_categories = [{
      category: "app-startup",
      entry: DESCRIPTION,
      value: CONTRACTID,
      service: true
    },{
      category: "content-policy",
      entry: CONTRACTID,
      value: CONTRACTID,
      service: true
    }];
service.prototype.QueryInterface = XPCOMUtils.generateQI([
      Ci.nsIObserver,
      Ci.nsISupports,
      Ci.nsISupportsWeakReference,
      Ci.uwIGreasemonkeyService,
      Ci.nsIWindowMediatorListener,
    ]);

///////////////////////////////// nsIObserver //////////////////////////////////

service.prototype.observe = function(aSubject, aTopic, aData) {
  switch (aTopic) {
    case 'app-startup':
    case 'profile-after-change':
      startup();
      break;
  }
};

//////////////////////////// uwIGreasemonkeyService ////////////////////////////

service.prototype.contentDestroyed = function(contentWindowId) {
  if (!contentWindowId) return;
  this.withAllMenuCommandsForWindowId(null, function(index, command) {
    var closed = false;
    try { closed = command.contentWindow.closed; } catch (e) { }

    if (closed || (command.contentWindowId == contentWindowId)) {
      gMenuCommands.splice(index, 1);
    }
  }, true);
};

service.prototype.contentFrozen = function(contentWindowId) {
  if (!contentWindowId) return;
  this.withAllMenuCommandsForWindowId(contentWindowId,
      function(index, command) { command.frozen = true; });
};

service.prototype.contentThawed = function(contentWindowId) {
  if (!contentWindowId) return;
  this.withAllMenuCommandsForWindowId(contentWindowId,
      function(index, command) { command.frozen = false; });
};

service.prototype.runScripts = function(aRunWhen, aWrappedContentWin, aChromeWin) {
  if (!this._uw.permissions_is_integration_allowed())
    return;

  var url = aWrappedContentWin.document.location.href;
  if (!UW_util.isGreasemonkeyable(url)) return;

  if (aWrappedContentWin.parent !== aWrappedContentWin) {
      return;
  }

  var scripts = [];

  var names = this._uwr.application_repository_resolve_url_as_json(this._repo, url);
  if (!names.isNull()) {
    let tmp = names.readString();
    this._uwr.g_free(names);
    names = tmp;
  } else {
    names = null;
  }

  if (names) {
    names = JSON.parse(names);
    for (var idx = 0; idx < names.length; ++idx) {
      var name = names[idx];

      let appDomain = this._uwr.application_repository_get_resolved_application_domain (this._repo, name);
      if (!appDomain.isNull() && this._uw.permissions_get_domain_dontask(appDomain.readString()))
        continue;

      switch (this._uwr.application_repository_get_resolved_application_status(this._repo, name)) {
      case this._uwr.APPLICATION_STATUS_AVAILABLE:
        suggestAppInstall(this._uw, this._uwr, this._repo, aChromeWin, aWrappedContentWin, name, url);
        break;
      case this._uwr.APPLICATION_STATUS_INSTALLED:
        var src = getScriptSource(this._uwr, this._repo, name);
        scripts.push({ name: name, content: src });
        break;
      }
    }
  }

  if (scripts.length > 0) {
    this.injectScripts(scripts, url, aWrappedContentWin, aChromeWin);
  }
};

service.prototype.ignoreNextScript = function() {
  this._ignoreNextScript = true;
};

service.prototype.injectScripts = function(
    scripts, url, wrappedContentWin, chromeWin
) {
  var firebugConsole = getFirebugConsole(wrappedContentWin, chromeWin);

  for (var i = 0, script = null; script = scripts[i]; i++) {
    
    var sandbox = createSandbox(script.name, wrappedContentWin, chromeWin, firebugConsole, url);
    // These newlines are critical for error line calculation.  The last handles
    // a script whose final line is a line comment, to not break the wrapper
    // function.
    runScriptInSandbox(script.content, sandbox);
  }
};

service.prototype.withAllMenuCommandsForWindowId = function(
    aContentWindowId, aCallback, aForce
) {
  if(!aContentWindowId && !aForce) return;

  var l = gMenuCommands.length - 1;
  for (var i = l, command = null; command = gMenuCommands[i]; i--) {
    if (aForce
        || (command.contentWindowId == aContentWindowId)
    ) {
      aCallback(i, command);
    }
  }
};

var NSGetFactory = XPCOMUtils.generateNSGetFactory([service]);
