/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is the Extension Manager.
 *
 * The Initial Developer of the Original Code is Ben Goodger.
 * Portions created by the Initial Developer are Copyright (C) 2004
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *  Ben Goodger <ben@bengoodger.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

var gExtensionManager = null;

function nsExtensionManager()
{
  var os = Components.classes["@mozilla.org/observer-service;1"]
                     .getService(Components.interfaces.nsIObserverService);
  os.addObserver(this, "profile-after-change", false);
  
  dump("*** global extensions startup!\n");
}

nsExtensionManager.prototype = {

  // nsIObserver
  observe: function (aSubject, aTopic, aData)
  {
    if (aTopic == "profile-after-change") {
      var os = Components.classes["@mozilla.org/observer-service;1"]
                        .getService(Components.interfaces.nsIObserverService);
      os.removeObserver(this, "profile-after-change");
      
      // Check to see if the version of the application that is being started
      // now is the same one that was started last time. 
      var pref = Components.classes["@mozilla.org/preferences-service;1"]
                           .getService(Components.interfaces.nsIPrefBranch);
      var currAppVersion = pref.getCharPref(PREF_EM_APP_VERSION);
      var lastAppVersion = pref.getCharPref(PREF_EM_LAST_APP_VERSION);
      if (currAppVersion != lastAppVersion) {
        // Version mismatch, we're have to load the extensions datasource
        // and do version checking. 
        var power = currAppVersion.split(".").length;
        currAppVersion = this.parseVersion(currAppVersion, power);
        
        var extensions = ds.getIncompatibleExtensionList(currAppVersion);
        var updater = new nsExtensionUpdater(extensions, appID, currAppVersion);
        updater.checkForUpdates(doneUpdatingExtension);
      }

      dump("*** profile extensions startup\n");
    }
  },
  
  parseVersion: function (aVersion, aPower)
  {
    var parts = aVersion.split(".");
    var vrsion = 0;
    if (aPower == 0)
      aPower = parts.length;
    
    for (var i = 0; i < parts.length; ++i) {
      var token = parts[i];
      if (token.charAt(token.length-1) == "+") {
        token = token.substr(0, token.lastIndexOf("+"));
        version += 1;
        if (token.length == 0)
          continue;
      }
      
      version += parseInt(token) * Math.pow(10, aPower - i);
    }
    return version;
  },

  // nsIExtensionManager
  installExtensionFromStream: function (aStream, aUseProfile)
  {
    var parser = Components.classes["@mozilla.org/rdf/xml-parser;1"]
                           .createInstance(Components.interfaces.nsIRDFXMLParser);
    var ds = Components.classes["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"]
                       .createInstance(Components.interfaces.nsIRDFDataSource);
    var streamListener = parser.parseAsync(ds, null);
    
    var bytesAvailable;
    do {
      bytesAvailable = aStream.available();
      if (!bytesAvailable)
        break;
      
      streamListener.onDataAvailable(null, null, aStream, 0, bytesAvailable);
    }
    while (1);
    
    this._ensureDS();
    
    this._ds.installExtension(ds, aUseProfile);
  },
      
  uninstallExtension: function (aExtensionID)
  {
    this._ds.uninstallExtension(aExtensionID);
  },
  
  enableExtension: function (aExtensionID)
  {
    this._ds.enableExtension(aExtensionID);
  },
  
  disableExtension: function (aExtensionID)
  {
    this._ds.disableExtension(aExtensionID);
  },
  
  updateExtension: function (aExtensionID)
  {
    var pref = Components.classes["@mozilla.org/preferences-service;1"]
                         .getService(Components.interfaces.nsIPrefBranch);
    var appID = pref.getCharPref(PREF_EM_APP_ID);
    var appVersion = pref.getCharPref(PREF_EM_APP_VERSION);

    var extensions = this._ds.getExtensionList(aExtensionID);
    var updater = new nsExtensionUpdater(extensions, appID, appVersion);
    updater.checkForUpdates();
  },  
  
  // Themes
  installTheme: function (aThemeID)
  {
  
  },
  
  uninstallTheme: function (aThemeID)
  {
  
  },

  updateTheme: function (aThemeID)
  {
  
  },
  
  get datasource()
  {
    this._ensureDS();
    return this._ds;
  },
  
  //
  _ds: null,
    
  // Other
  _ensureDS: function ()
  {
    if (!this._ds) {
      this._ds = new nsExtensionsDataSource();
      if (this._ds) {
        this._ds.loadExtensions(false);
        this._ds.loadExtensions(true);
      }
    }
  },

  /////////////////////////////////////////////////////////////////////////////
  // nsISupports
  QueryInterface: function (aIID) 
  {
    if (!aIID.equals(Components.interfaces.nsIExtensionManager) &&
        !aIID.equals(Components.interfaces.nsIObserver) &&
        !aIID.equals(Components.interfaces.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  }
};

function nsExtensionUpdater(aExtensions, 
                            aTargetAppID, aTargetAppVersion) 
{
  this._extensions = aExtensions;
  this._count = aExtensions.length;
  this._appID = aTargetAppID;
  this._appVersion = aTargetAppVersion;

  this._os = Components.classes["@mozilla.org/observer-service;1"]
                       .getService(Components.interfaces.nsIObserverService);
}

nsExtensionUpdater.prototype = {
  /////////////////////////////////////////////////////////////////////////////
  // 
  checkForUpdates: function () 
  {
    var wspFactory = Components.classes["@mozilla.org/xmlextras/proxy/webserviceproxyfactory;1"]
                              .getService(Components.interfaces.nsIWebServiceProxyFactory);
    wspFactory.createProxyAsync("http://localhost:8080/axis/services/VersionCheck?wsdl", 
                                "VersionCheck", "", true, this);
  },

  _proxy: null,
  
  _getExtensionsToUpdate: function ()
  {
    this._proxy.getExtensionsToUpdate(this._extensions, this._appID, this._appVersion);
  },
  
  _checkForUpdates: function ()
  {
    this._os.notifyObservers(null, "update-started", "");
    //
    for (var i = 0; i < this._extensions.length; ++i) {
      var e = this._extensions[i];
      this._os.notifyObservers(null, "update-item-started", e.name);
      this._proxy.getNewestExtension(e, this._appID, this._appVersion);
      //this._proxy.getExtension(e.id, e.version, this._appID, this._appVersion);
    }
  },

  /////////////////////////////////////////////////////////////////////////////
  // nsIWSDLLoadListener  
  onLoad: function (aProxy)
  { 
    this._proxy = aProxy;
    this._proxy.setListener(this);
    // this._getExtensionsToUpdate();
    this._checkForUpdates();
  },
  
  onError: function (aError)
  {
    dump("*** onError ERROR = " + aError + "\n");
  },
  
  getExtensionsToUpdateCallback: function (aResult)
  {
  },

  getExtensionCallback: function (aResult)
  {
    dump("*** getExtensionCallback RESULT = " + aResult + "\n");
  },
  
  getNewestExtensionCallback: function (aResult)
  {
    dump("*** getNewestExtensionCallback RESULT = " + aResult + "\n");
    
    this._os.notifyObservers(null, "update-item-ended", "goat");
    
    if (--this._count == 0)
      this._os.notifyObservers(null, "update-ended", "");
  }
};


const PREF_EM_DEFAULTUPDATEURL = "update.url.extensions";
const PREF_EM_APP_ID = "app.id";
const PREF_EM_LAST_APP_VERSION = "extensions.lastAppVersion";
const PREF_EM_APP_VERSION = "general.useragent.vendorSub";

function EM_NS(aProperty)
{
  return "http://www.mozilla.org/2004/em-rdf#" + aProperty;
}

function nsExtensionsDataSource()
{
  this._rdf = Components.classes["@mozilla.org/rdf/rdf-service;1"]
                        .getService(Components.interfaces.nsIRDFService);
}

nsExtensionsDataSource.prototype = {
  _rdf: null,
  
  _emR: function (aProperty) 
  {
    return this._rdf.GetResource(EM_NS(aProperty));
  },
  
  _emL: function (aLiteral)
  {
    return this._rdf.GetLiteral(aLiteral);
  },

  _stripPrefix: function (aResourceURI)
  {
    return aResourceURI.substr("urn:mozilla:extension:".length, aResourceURI.length);
  },

  getExtensionList: function (aExtensionID)
  {
    var extensions = [];
    if (aExtensionID) {
      extensions.push({ id: aExtensionID, 
                        version: this.getExtensionProperty(aExtensionID, "version"),
                        name: this.getExtensionProperty(aExtensionID, "name"), 
                        row: -1, xpiURL: "" });
    }
    else {
      var ctr = Components.classes["@mozilla.org/rdf/container;1"]
                          .createInstance(Components.interfaces.nsIRDFContainer);
      ctr.Init(this, this._rdf.GetResource("urn:mozilla:extension:root"));
      
      var elements = ctr.GetElements();
      while (elements.hasMoreElements()) {
        var e = elements.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
        var id = this._stripPrefix(e.Value);
        extensions.push({ id: id, version: this.getExtensionProperty(id, "version"), 
                          name: this.getExtensionProperty(id, "name"), row: -1, 
                          xpiURL: "" });
      }
    }
    return extensions;
  },
  
  _setProperty: function (aDS, aSource, aProperty, aNewValue)
  {
    var oldValue = aDS.GetTarget(aSource, aProperty, true);
    if (oldValue)
      aDS.Change(aSource, aProperty, oldValue, aNewValue);
    else
      aDS.Assert(aSource, aProperty, aNewValue, true);
  },
  
  getExtensionProperty: function (aExtensionID, aProperty)
  {
    var extension = this._rdf.GetResource("urn:mozilla:extension:" + aExtensionID);
    return this.GetTarget(extension, this._emR(aProperty), true).QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
  },
  
  _setExtensionProperty: function (aExtensionID, aPropertyArc, aPropertyValue)
  {
    var extension = this._rdf.GetResource("urn:mozilla:extension:" + aExtensionID);
    var installLocation = this.GetTarget(extension, this._emR("installLocation"), true);
    var isProfile = installLocation.EqualsNode(this._emR("profile"));
    var ds = isProfile ? this._profileExtensions : this._appExtensions;
    
    this._setProperty(ds, extension, aPropertyArc, aPropertyValue);

    this._flush(isProfile);  
  },
  
  enableExtension: function (aExtensionID)
  {
    this._setExtensionProperty(aExtensionID, this._emR("toBeEnabled"), this._emL("true"));
    this._setExtensionProperty(aExtensionID, this._emR("toBeDisabled"), this._emL("false"));
    this._setExtensionProperty(aExtensionID, this._emR("disabled"), this._emL("false"));
  },
  
  disableExtension: function (aExtensionID)
  {
    this._setExtensionProperty(aExtensionID, this._emR("toBeDisabled"), this._emL("true"));
    this._setExtensionProperty(aExtensionID, this._emR("toBeEnabled"), this._emL("false"));
    this._setExtensionProperty(aExtensionID, this._emR("disabled"), this._emL("true"));
  },
  
  uninstallExtension: function (aExtensionID)
  {
    var ctr = Components.classes["@mozilla.org/rdf/container;1"]
                        .createInstance(Components.interfaces.nsIRDFContainer);
    ctr.Init(this, this._rdf.GetResource("urn:mozilla:extension:root"));
    
    var extension = this._rdf.GetResource("urn:mozilla:extension:" + aExtensionID);
    ctr.RemoveElement(extension, true);
    
    this._setExtensionProperty(aExtensionID, this._emR("toBeUninstalled"), this._emL("true"));
  },
  
  getUpdateURLs: function (aExtensionID)
  {
    var pref = Components.classes["@mozilla.org/preferences-service;1"]
                         .getService(Components.interfaces.nsIPrefBranch);
    var appID = pref.getCharPref(PREF_EM_APP_ID);
  
    var urls = [];
    if (aExtensionID) {
      var updateURL = this._getUpdateURLInternal(aExtensionID);
      updateURL = updateURL.replace(/%APP%/g, escape(appID));
      updateURL = updateURL.replace(/%ITEM%/g, escape(aExtensionID));
      urls.push(updateURL);
    }
    else {
      var ctr = Components.classes["@mozilla.org/rdf/container;1"]
                          .createInstance(Components.interfaces.nsIRDFContainer);
      ctr.Init(this, this._rdf.GetResource("urn:mozilla:extension:root"));
      
      var urlHash = { };
      
      var e = ctr.GetElements();
      while (e.hasMoreElements()) {
        var r = e.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
        var extensionID = r.Value.substr("urn:mozilla:extension:".length, r.Value.length);
        var updateURL = this._getUpdateURLInternal(extensionID);
        if (!(updateURL in urlHash))
          urlHash[updateURL] = [];
          
        urlHash[updateURL].push(extensionID);
      }
      
      for (var url in urlHash) {
        var guidString = "";
        var urlCount = urlHash[url].length;
        for (var i = 0; i < urlCount; ++i)
          guidString += escape(urlHash[url][i] + (i < urlCount - 1 ? "," : ""));
        url = url.replace(/%APP%/g, appID);
        url = url.replace(/%ITEM%/g, guidString);
        urls.push(url);
      }
    }
    return urls;
  },
  
  _getUpdateURLInternal: function (aExtensionID)
  {
    var updateURL;
    var extension = this._rdf.GetResource("urn:mozilla:extension:" + aExtensionID);
   
    if (this.hasArcOut(extension, this._emR("updateURL"))) {
      updateURL = this.GetTarget(extension, this._emR("updateURL"), true);
      if (updateURL) 
        updateURL = updateURL.QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
    }
    
    if (!updateURL) {
      var pref = Components.classes["@mozilla.org/preferences-service;1"]
                           .getService(Components.interfaces.nsIPrefBranch);
      updateURL = pref.getCharPref(PREF_EM_DEFAULTUPDATEURL);
    }
    return updateURL;
  },
  
  loadExtensions: function (aProfile)
  {
    var fileLocator = Components.classes["@mozilla.org/file/directory_service;1"].getService(Components.interfaces.nsIProperties);
    var key = aProfile ? "ProfD" : "ProfD"; // XCurProcDir
    var extensionsFile = fileLocator.get(key, Components.interfaces.nsIFile);
    extensionsFile.append("Extensions");
    extensionsFile.append("extensions.rdf");
    
    if (!extensionsFile.exists())
      return;

    var ioServ = Components.classes["@mozilla.org/network/io-service;1"]
                           .getService(Components.interfaces.nsIIOService);
    var fph = ioServ.getProtocolHandler("file").QueryInterface(Components.interfaces.nsIFileProtocolHandler);
    var dsURL = fph.getURLSpecFromFile(extensionsFile);
    
    var ds = this._rdf.GetDataSourceBlocking(dsURL);
    
    if (aProfile) {
      this._profileExtensions = ds;
      if (!this._composite) 
        this._composite = Components.classes["@mozilla.org/rdf/datasource;1?name=composite-datasource"]
                                    .createInstance(Components.interfaces.nsIRDFDataSource);
      if (this._appExtensions)
        this._composite.RemoveDataSource(this._appExtensions);
      this._composite.AddDataSource(this._profileExtensions);
      if (this._appExtensions)
        this._composite.AddDataSource(this._appExtensions);  
    }
    else {
      this._appExtensions = ds;
      
      if (!this._composite)
        this._composite = Components.classes["@mozilla.org/rdf/datasource;1?name=composite-datasource"]
                                    .createInstance(Components.interfaces.nsIRDFCompositeDataSource);
      this._composite.AddDataSource(this._appExtensions);
    }
  },
  
  /////////////////////////////////////////////////////////////////////////////
  // nsIRDFDataSource

  _appExtensions: null,
  _profileExtensions: null,  
  _composite: null,
  
  get URI()
  {
    return "rdf:extensions";
  },
  
  GetSource: function (aProperty, aTarget, aTruthValue)
  {
    return this._composite.GetSource(aProperty, aTarget, aTruthValue);
  },
  
  GetSources: function (aProperty, aTarget, aTruthValue)
  {
    return this._composite.GetSources(aProperty, aTarget, aTruthValue);
  },
  
  GetTarget: function (aSource, aProperty, aTruthValue)
  {
    if (aProperty.EqualsNode(this._emR("iconURL"))) {
      var hasIconURL = this._composite.hasArcOut(aSource, aProperty);
      // If the download entry doesn't have a IconURL property, use a
      // generic icon URL instead.
      if (!hasIconURL)
        return this._rdf.GetResource("chrome://mozapps/skin/xpinstall/xpinstallItemGeneric.png");
    }
    else if (aProperty.EqualsNode(this._emR("installLocation"))) {
      var hasNameArc = this._profileExtensions.hasArcOut(aSource, this._emR("name"));
      var hasVersionArc = this._profileExtensions.hasArcOut(aSource, this._emR("version"));
      return hasNameArc && hasVersionArc ? this._emL("profile") : this._emL("global");
    }
    
    return this._composite.GetTarget(aSource, aProperty, aTruthValue);
  },
  
  GetTargets: function (aSource, aProperty, aTruthValue)
  {
    return this._composite.GetTargets(aSource, aProperty, aTruthValue);
  },
  
  Assert: function (aSource, aProperty, aTarget, aTruthValue)
  {
    return Components.results.NS_RDF_ASSERTION_REJECTED;
  },
  
  Unassert: function (aSource, aProperty, aTarget)
  {
    return Components.results.NS_RDF_ASSERTION_REJECTED;
  },
  
  Change: function (aSource, aProperty, aOldTarget, aNewTarget)
  {
    return Components.results.NS_RDF_ASSERTION_REJECTED;
  },

  Move: function (aSource, aNewSource, aProperty, aTarget)
  {
    return Components.results.NS_RDF_ASSERTION_REJECTED;
  },
  
  HasAssertion: function (aSource, aProperty, aTarget, aTruthValue)
  {
    return this._composite.HasAssertion(aSource, aProperty, aTarget, aTruthValue);
  },
  
  AddObserver: function (aObserver)
  {
    this._composite.AddObserver(aObserver);
  },
  
  RemoveObserver: function (aObserver)
  {
    this._composite.RemoveObserver(aObserver);
  },
  
  ArcLabelsIn: function (aNode)
  {
    return this._composite.ArcLabelsIn(aNode);
  },
  
  ArcLabelsOut: function (aSource)
  {
    return this._composite.ArcLabelsOut(aSource);
  },
  
  GetAllResources: function ()
  {
    return this._composite.GetAllResources();
  },
  
  IsCommandEnabled: function (aSources, aCommand, aArguments)
  {
    return this._composite.IsCommandEnabled(aSources, aCommand, aArguments);
  },
  
  DoCommand: function (aSources, aCommand, aArguments)
  {
    this._composite.DoCommand(aSources, aCommand, aArguments);
  },
  
  GetAllCmds: function (aSource)
  {
    return this._composite.GetAllCmds(aSource);
  },
  
  hasArcIn: function (aNode, aArc)
  {
    return this._composite.hasArcIn(aNode, aArc);
  },
  
  hasArcOut: function (aSource, aArc)
  {
    return this._composite.hasArcOut(aSource, aArc);
  },
  
  beginUpdateBatch: function ()
  {
    return this._composite.beginUpdateBatch();
  },
  
  endUpdateBatch: function ()
  {
    return this._composite.endUpdateBatch();
  },
  
  /////////////////////////////////////////////////////////////////////////////
  // nsIRDFRemoteDataSource
  
  get loaded()
  {
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  },
  
  Init: function (aURI)
  {
  },
  
  Refresh: function (aBlocking)
  {
  },
  
  Flush: function ()
  {
    this._flush(false);
    this._flush(true);
  },
  
  FlushTo: function (aURI)
  {
  },
  
  _flush: function (aIsProfile)
  { 
    var ds = aIsProfile ? this._profileExtensions : this._appExtensions;
    var rds = ds.QueryInterface(Components.interfaces.nsIRDFRemoteDataSource);
    rds.Flush();
  },

  /////////////////////////////////////////////////////////////////////////////
  // nsISupports
  QueryInterface: function (aIID) 
  {
    if (!aIID.equals(Components.interfaces.nsIRDFDataSource) &&
        !aIID.equals(Components.interfaces.nsIRDFRemoteDataSource) && 
        !aIID.equals(Components.interfaces.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  }
};


var gModule = {
  _firstTime: true,
  
  registerSelf: function (aComponentManager, aFileSpec, aLocation, aType) 
  {
    if (this._firstTime) {
      this._firstTime = false;
      throw Components.results.NS_ERROR_FACTORY_REGISTER_AGAIN;
    }
    aComponentManager = aComponentManager.QueryInterface(Components.interfaces.nsIComponentRegistrar);
    
    aComponentManager.registerFactoryLocation(this._cid, 
                                              "Extension Manager",
                                              this._contractId,
                                              aFileSpec,
                                              aLocation,
                                              aType);
    
    // Make the Extension Manager a startup observer
    var categoryManager = Components.classes["@mozilla.org/categorymanager;1"]
                                    .getService(Components.interfaces.nsICategoryManager);
    categoryManager.addCategoryEntry("app-startup", "Extension Manager", 
                                     "service," + this._contractId, true, true, null);
  },
  
  getClassObject: function (aComponentManager, aCID, aIID) 
  {
    if (!aCID.equals(this._cid))
      throw Components.results.NS_ERROR_NO_INTERFACE;
      
    if (!aIID.equals(Components.interfaces.nsIFactory))
      throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
    
    return this._factory;
  },

  _cid: Components.ID("{8A115FAA-7DCB-4e8f-979B-5F53472F51CF}"),

  _contractId: "@mozilla.org/extension-manager;1",
  
  _factory: {
    createInstance: function (aOuter, aIID) 
    {
      if (aOuter != null)
        throw Components.results.NS_ERROR_NO_AGGREGATION;
      
      if (!gExtensionManager)
        gExtensionManager = new nsExtensionManager();
        
      return gExtensionManager.QueryInterface(aIID);
    }
  },
  
  canUnload: function (aComponentManager) 
  {
    return true;
  }
};

function NSGetModule(compMgr, fileSpec) 
{
  return gModule;
}
