/* ***** 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 XUL application document pack.
 *
 * The Initial Developer of the Original Code is
 * Alexander J. Vincent <ajvincent@gmail.com>.
 * Portions created by the Initial Developer are Copyright (C) 2007
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * 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 ***** */

// Utility functions.
function ERROR_FAILURE() {
  throw Components.results.NS_ERROR_FAILURE;
}

function NOT_IMPLEMENTED() {
  dumpJSStack();
  throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
}

function ERROR_ABORT() {
  throw Components.results.NS_ERROR_ABORT;
}

function NOT_INITIALIZED() {
  throw Components.results.NS_ERROR_NOT_INITIALIZED;
}

function INVALID_POINTER() {
  throw Components.results.NS_ERROR_INVALID_POINTER;
}

var C_i = Components.interfaces;

const xeIChromeDocumentPack       = C_i.xeIChromeDocumentPack;
const xeIDocumentPack             = C_i.xeIDocumentPack;
const xeIXMLDocumentWrapper       = C_i.xeIXMLDocumentWrapper;
const xeIDocumentWrapper          = C_i.xeIDocumentWrapper;
const xeIDTDWrapper               = C_i.xeIDTDWrapper;
const xeIDocumentFile             = C_i.xeIDocumentFile;
const xeIBaseURIMap               = C_i.xeIBaseURIMap;
const xeIFileSearchEngine         = C_i.xeIFileSearchEngine;
const xeIFileSearchResult         = C_i.xeIFileSearchResult;
const xeIEntityManager            = C_i.xeIEntityManager;
const nsIDOMWindow                = C_i.nsIDOMWindow;
const nsISimpleEnumerator         = C_i.nsISimpleEnumerator;
const nsICategoryManager          = C_i.nsICategoryManager;
const nsIFileProtocolHandler      = C_i.nsIFileProtocolHandler;
const mozIJSSubScriptLoader       = C_i.mozIJSSubScriptLoader;
const nsIProgrammingLanguage      = C_i.nsIProgrammingLanguage;
const nsISecurityCheckedComponent = C_i.nsISecurityCheckedComponent;
const nsIClassInfo                = C_i.nsIClassInfo;
const nsIComponentRegistrar       = C_i.nsIComponentRegistrar;
const nsIFactory                  = C_i.nsIFactory;
const nsIModule                   = C_i.nsIModule;
const nsISupports                 = C_i.nsISupports;

C_i = null;

const fileClass = "@mozilla.org/network/protocol;1?name=file";
const fileProtocol  = Components.classes[fileClass]
                                .getService(nsIFileProtocolHandler);
const catMan = Components.classes["@mozilla.org/categorymanager;1"]
                         .getService(nsICategoryManager);
const catName = "Verbosio document wrappers by file type";

var jsLoader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
                         .getService(mozIJSSubScriptLoader);
jsLoader.loadSubScript("resource://app/res/ecma-debug.js", this);
function loadSubscripts() {
}

var Module = {
  factoriesByClassId: {},

  constructorsArray: [],

  /**
   * Add a component constructor to the module.
   *
   * @param aConstructor JavaScript function implementing nsIClassInfo
   */
  addConstructor: function addConstructor(aConstructor) {
    var cid = aConstructor.prototype.classID.toString();
    this.factoriesByClassId[cid] = {
      // nsIFactory
      createInstance: function createInstance(outer, iid) {
        if (outer != null)
          throw Components.results.NS_ERROR_NO_AGGREGATION;
        return (new aConstructor()).QueryInterface(iid);
      },

      // nsISupports
      QueryInterface: function QueryInterface(aIID) {
        if (aIID.equals(nsIFactory) ||
            aIID.equals(nsISupports))
          return this;

        throw Components.results.NS_ERROR_NO_INTERFACE;
        return null;
      }
    };

    this.constructorsArray[this.constructorsArray.length] = aConstructor;
  },

  servicesArray: [],

  /**
   * Add a component constructor to the module.
   *
   * @param aConstructor JavaScript function implementing nsIClassInfo
   */
  addService: function addService(aService) {
    var cid = aService.classID.toString();
    this.factoriesByClassId[cid] = {
      // nsIFactory
      createInstance: function createInstance(outer, iid) {
        if (outer != null)
          throw Components.results.NS_ERROR_NO_AGGREGATION;
        return aService.QueryInterface(iid);
      },

      // nsISupports
      QueryInterface: function QueryInterface(aIID) {
        if (aIID.equals(nsIFactory) ||
            aIID.equals(nsISupports))
          return this;

        throw Components.results.NS_ERROR_NO_INTERFACE;
        return null;
      }
    };

    this.servicesArray[this.servicesArray.length] = aService;
  },

  categoriesArray: [],

  /**
   * Add a category for the category manager to accept at registration time.
   *
   * @param aCategory   The category to add.
   * @param aEntryName  The name of the entry.
   * @param aEntryValue The value of the entry (usually a component class id).
   */
  addCategory: function addCategory(aCategory, aEntryName, aEntryValue) {
    this.categoriesArray[this.categoriesArray.length] = {
      category: aCategory,
      name: aEntryName,
      value: aEntryValue
    };
  },

  // nsIModule
  registerSelf: function registerSelf(compMgr, fileSpec, location, type) {
    compReg = compMgr.QueryInterface(nsIComponentRegistrar);
    var i;

    for (i = 0; i < this.constructorsArray.length; i++) {
      var proto = this.constructorsArray[i].prototype;
      compReg.registerFactoryLocation(proto.classID,
                                      proto.classDescription,
                                      proto.contractID,
                                      fileSpec,
                                      location,
                                      type);
    }

    for (i = 0; i < this.servicesArray.length; i++) {
      var service = this.servicesArray[i];
      dump("Registering " + service.contractID + "\n");
      compReg.registerFactoryLocation(service.classID,
                                      service.classDescription,
                                      service.contractID,
                                      fileSpec,
                                      location,
                                      type);
    }

    if (!this.categoriesArray.length) {
      return;
    }
    var catman = Components.classes["@mozilla.org/categorymanager;1"]
                           .getService(nsICategoryManager);
    for (i = 0; i < this.categoriesArray.length; i++) {
      var entry = this.categoriesArray[i];
      catman.addCategoryEntry(entry.category,
                              entry.name,
                              entry.value,
                              true,
                              true);
    }
  },

  // nsIModule
  getClassObject: function getClassObject(compMgr, aCID, aIID) {
    if (!aIID.equals(nsIFactory)) {
      throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
    }

    for (var prop in this.factoriesByClassId) {
      if (aCID.equals(Components.ID(prop))) {
        loadSubscripts();
        return this.factoriesByClassId[prop];
      }
    }
    throw Components.results.NS_ERROR_NO_INTERFACE;
  },

  // nsIModule
  canUnload: function canUnload() {
    return true;
  },

  // nsISupports
  QueryInterface: function QueryInterface(aIID) {
    if (aIID.equals(nsIModule) ||
       (aIID.equals(nsISupports)))
      return this;

    throw Components.results.NS_ERROR_NO_INTERFACE;
    return null;
  }
};

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

function DocumentPack() {}
DocumentPack.prototype = {
  /**
   * Start up the document pack.
   */
  startup: function startup() {
    this.basePath = null;

    // xeIDocumentPack
    const nsITransactionManager = Components.interfaces.nsITransactionManager;
    this.txmgr = Components.classes["@mozilla.org/transactionmanager;1"]
                           .createInstance(nsITransactionManager);

    // xeIDocumentPack
    const entityMgrClass = "@verbosio.mozdev.org/dtd-entity-manager;1";
    this.entityManager = Components.classes[entityMgrClass]
                                   .createInstance(xeIEntityManager);
    this.entityManager.initialize(this);

    // xeIDocumentPack
    this.pendingAction = null;

    // xeIDocumentPack
    this.txlistener = null;

    this._wrappers = [];
    this._appPath = null;
    this._initialized = false;

    this._baseURIMaps   = {};
    this._contentURIMaps = {};
    /* this._contentURIMaps:
         - verbosio     (maps to chrome://verbosio/content/)
         - foo          (maps to chrome://foo/content/)
     */

    this._localeURIMaps = {};
    /* this._localeURIMaps:
         - en-US
           - verbosio   (maps to chrome://verbosio/locale/ for en-US localization)
           - foo        (maps to chrome://foo/locale/ for en-US localization)
         - en-GB
           - verbosio   (maps to chrome://verbosio/locale/ for en-GB localization)
           - foo        (maps to chrome://foo/locale/ for en-GB localization)
     */

    this._skinURIMaps   = {};
    /* this._skinURIMaps:
         - classic
           - verbosio   (maps to chrome://verbosio/skin/ for classic skin)
           - foo        (maps to chrome://foo/skin/ for classic skin)
         - modern
           - verbosio   (maps to chrome://verbosio/skin/ for modern skin)
           - foo        (maps to chrome://foo/skin/ for modern skin)
     */

    // The selected locale and skin Verbosio will use for resolving chrome URI's.
    this._selectedLocale = null;
    this._selectedSkin   = null;
  },

  /**
   * Add properties.
   *
   * @param aObject Object holding the properties to add.
   */
  addProperties: function addProperties(aObject) {
    for (var prop in aObject) {
      this[prop] = aObject[prop];
    }
  },

  checkInitialized: function checkInitialized() {
    if (!this._initialized) {
      NOT_INITIALIZED();
    }
  },

  /**
   * Determine the base URI's for content, skins and locales a manifest provides.
   *
   * @param aPath Path to chrome.manifest file.
   */
  processManifest: function processManifest(aPath) {
    // Read the manifest.
    const fileClass = "@verbosio.mozdev.org/document-file;1?"
                    + "contentType=text/plain";
    var docFile = Components.classes[fileClass]
                            .createInstance(xeIDocumentFile);
    docFile.init(aPath, "text/plain");
    docFile.read();
    var contents = docFile.savedContents.split("\n");
    docFile = null;

    // Parse the manifest; split it into its various components.
    var packageName;
    var packageType;
    var subType;
    var packagePath;
    var slash = (this._appPath.indexOf("/") == -1) ? "\\" : "/";
    for (var i = 0; i < contents.length; i++) {
      if (!(/\S/.test(contents[i]))) {
        continue;
      }
      var storage = null;
      var lineValues = contents[i].match(/\S+/g);
      packageType = lineValues[0];
      packageName = lineValues[1];
      packagePath = null;

      // Determine which subpackage of chrome the new map goes into.
      switch (packageType) {
        case "content":
          subType = null;
          storage = this._contentURIMaps;
          packagePath = lineValues[2];
          break;

        case "locale":
          subType = lineValues[2];
          if (!(subType in this._localeURIMaps)) {
            this._localeURIMaps[subType] = {};
          }
          if (!this._selectedLocale) {
            // Store the default locale for this application.
            this._selectedLocale = subType;
          }
          storage = this._localeURIMaps[subType];
          packagePath = lineValues[3];
          break;

        case "skin":
          subType = lineValues[2];
          if (!(subType in this._skinURIMaps)) {
            this._skinURIMaps[subType] = {};
          }
          if (!this._selectedSkin) {
            // Store the default skin for this application.
            this._selectedSkin = subType;
          }
          storage = this._skinURIMaps[subType];
          packagePath = lineValues[3];
          break;
        default:
          // ignore it for now.
      } // end switch (packageType);

      if (!storage) {
        continue;
      }

      /* We have a package name ("foo"), a package type ("content"),
         a path to the chrome.manifest file (aPath), and a path from the
         manifest's parent directory to the package (packagePath).  From this,
         we can construct the chrome path ("chrome://foo/content/") and the
         local path ("/home/username/appname/objdir/chrome/content/foo/").
         These are the ingredients for the base URI map for chrome files.
       */
      assert(packageName, "What chrome package does this belong to?");
      assert(!(packageName in storage), "Package already defined!");
      assert(packagePath, "No package path?");
      currentMap = Components.classes["@verbosio.mozdev.org/base-uri-map;1"]
                             .createInstance(xeIBaseURIMap);
      var chromePath = "chrome://" + packageName + "/" + packageType + "/";
      currentMap.addType("chrome", chromePath);
      var dirPath = aPath.substr(0, aPath.length - ("chrome.manifest").length);
      assert(dirPath.charAt(dirPath.length - 1) == slash, "Check slash!");

      var localPath;
      if (packagePath.indexOf("jar:") == 0) {
        // Jarred chrome means we have to work harder and more carefully.
        var inJarPath = packagePath.substr(packagePath.indexOf(".jar!") + 5);
        assert(inJarPath, "Not a real jar file?");
        packagePath = packagePath.substring(4, packagePath.indexOf(inJarPath));

        if (slash == "\\") {
          packagePath = packagePath.replace(/\//g, "\\");
        }
        if (inJarPath.charAt(inJarPath.length - 1) != "/") {
          inJarPath += "/";
        }

        localPath = "jar:" + dirPath + packagePath + inJarPath;
      } else {
        // Local, unjarred file.
        if (slash == "\\") {
          packagePath = packagePath.replace(/\//g, "\\");
        }
        localPath = dirPath + packagePath;
        if (localPath.charAt(localPath.length - 1) != slash) {
          localPath += slash;
        }
      }

      currentMap.addType("local", localPath);

      // Add the base URI to the appropriate chrome subpackage.
      storage[packageName] = currentMap;
    } // end for (var i = 0; i < contents.length; i++)
  },

  // xeIChromeDocumentPack
  get selectedLocale() {
    this.checkInitialized();
    return this._selectedLocale;
  },
  set selectedLocale(val) {
    // this.checkInitialized();
    for (var prop in this._localeURIMaps) {
      if (val == prop) {
        this._selectedLocale = val;
        return val;
      }
    }
    ERROR_FAILURE();
    return null; // silence a strict warning... we'll never get here.
  },

  // xeIChromeDocumentPack
  get selectedSkin() {
    this.checkInitialized();
    return this._selectedSkin;
  },
  set selectedSkin(val) {
    // this.checkInitialized();
    for (var prop in this._skinURIMaps) {
      if (val == prop) {
        this._selectedSkin = val;
        return val;
      }
    }
    ERROR_FAILURE();
    return null; // silence a strict warning... we'll never get here.
  },

  /**
   * Gather the package names from a package map.
   *
   * @param aMap The package map to read names from.
   * @param aObject Generic map object to add entries to.
   */
  addChromePackageNames: function addChromePackageNames(aMap, aObject) {
    for (var prop in aMap) {
      aObject[prop] = true;
    }
  },

  /**
   * Complete a URI if it is incomplete.
   *
   * @param aPath The URI to complete.
   *
   * @return The corrected URI.
   */
  completeURI: function completeURI(aPath) {
    // chrome://foo/locale/ -> chrome://foo/locale/foo.dtd
    const chromeRE = /^chrome:\/\/([^\/]*)\/([^\/]*)\//;
    var chromePath = aPath.match(chromeRE);
    if (chromePath && aPath == chromePath[0]) {
      path = aPath + chromePath[1];
      switch (chromePath[2]) {
        case "content":
          path += ".xul";
          break;
        case "locale":
          path += ".dtd";
          break;
        case "skin":
          path += ".css";
          break;
        default:
          throw new Error("Unexpected package type!");
      }
    } else if (aPath.indexOf("file://") == 0) {
      path = fileProtocol.getFileFromURLSpec(aPath).path;
    } else {
      path = aPath;
    }
    return path;
  },

  // xeIChromeDocumentPack
  getChromePackageNames: function getChromePackageNames(aCount) {
    var map = this._contentURIMaps;
    var packageList = {};
    this.addChromePackageNames(map, packageList);

    var subPackage;
    for each (map in this._localeURIMaps) {
      this.addChromePackageNames(map, packageList);
    }

    for each (map in this._skinURIMaps) {
      this.addChromePackageNames(map, packageList);
    }

    var rv = [];
    for (var prop in packageList) {
      rv[rv.length] = prop;
    }
    aCount.value = rv.length;
    return rv;
  },

  // xeIChromeDocumentPack
  getSubPackageNames:
  function getSubPackageNames(aName, aType, aCount) {
    this.checkInitialized();
    var rvArray = [];
    var map = null;
    switch (aType) {
      case xeIChromeDocumentPack.PACKAGE_TYPE_CONTENT:
        if (aName in this._contentURIMaps) {
          // Return a null subpackage, to confirm the content package exists.
          rvArray[rvArray.length] = null;
        }
        break;

      case xeIChromeDocumentPack.PACKAGE_TYPE_LOCALE:
        for (var subPackage in this._localeURIMaps) {
          map = this._localeURIMaps[subPackage];
          if (aName in map) {
            rvArray[rvArray.length] = subPackage;
          }
        }
        break;

      case xeIChromeDocumentPack.PACKAGE_TYPE_SKIN:
        for (var subPackage in this._skinURIMaps) {
          map = this._skinURIMaps[subPackage];
          if (aName in map) {
            rvArray[rvArray.length] = subPackage;
          }
        }
        break;

      default:
        INVALID_POINTER();
    } // end switch (aType);

    aCount.value = rvArray.length;
    return rvArray;
  },

  // xeIChromeDocumentPack
  getChromeBaseURI: function getChromeBaseURI(aName, aType, aSubName) {
    this.checkInitialized();
    var map = null;
    switch (aType) {
      case xeIChromeDocumentPack.PACKAGE_TYPE_CONTENT:
        if (aSubName !== null) {
          break;
        }
        map = this._contentURIMaps;
        break;

      case xeIChromeDocumentPack.PACKAGE_TYPE_LOCALE:
        if (aSubName in this._localeURIMaps) {
          map = this._localeURIMaps[aSubName];
        }
        break;

      case xeIChromeDocumentPack.PACKAGE_TYPE_SKIN:
        if (aSubName in this._skinURIMaps) {
          map = this._skinURIMaps[aSubName]
        }
        break;
    }

    if (map && (aName in map)) {
      return map[aName];
    }

    INVALID_POINTER();
    return null;
  },

  // xeIDocumentPack
  item: function item(aIndex) {
    this.checkInitialized();
    if (aIndex >= this.length) {
      // XXX ajvincent Find a better error code than this.
      ERROR_FAILURE();
    }

    return this._wrappers[aIndex];
  },

  // xeIDocumentPack
  get length() {
    return this._wrappers.length;
  },

  wrapperContained: function wrapperContained(aWrapper) {
    for (var i = 0; i < this.length; i++) {
      if (this.item(i) == aWrapper)
        return true;
    }

    return false;
  },

  // xeIDocumentPack
  addWrapper: function addWrapper(aWrapper) {
    this.checkInitialized();
    if (!(aWrapper instanceof xeIDocumentWrapper)) {
      throw Components.results.NS_ERROR_INVALID_ARG;
    }

    if (this.wrapperContained(aWrapper)) {
      ERROR_ABORT();
    }

    // Sanity checks on the wrapper
    if (gDebugPref) {
      try {
        assert(aWrapper.baseURIMap instanceof xeIBaseURIMap, "No base URI!");
        assert(aWrapper.pathToDocument, "No path to the document!");
      } catch (e) {
        dump(e.stack + "\n\n");
        throw e;
      }
    }
    if ((aWrapper instanceof xeIXMLDocumentWrapper) ||
        (aWrapper instanceof xeIDTDWrapper)) {
      this.entityManager.mapEntities(aWrapper);
    }
    this._wrappers[this.length] = aWrapper;
  },

  // xeIDocumentPack
  removeWrapper: function removeWrapper(aWrapper) {
    this.checkInitialized();
    if (!aWrapper) {
      ERROR_ABORT();
    }
    if (!this.wrapperContained(aWrapper)) {
      ERROR_ABORT();
    }

    for (var i = 0; i < this.length; i++) {
      if (this.item(i) == aWrapper) {
        this._wrappers.splice(i, 1);
      }
    }
  },

  // xeIDocumentPack
  getViewerScopes: function getViewerScopes(aCount) {
    this.checkInitialized();
    var scopes = ["dir-contents@pack-viewers.verbosio.mozdev.org",
                  "chrome-contents@pack-viewers.verbosio.mozdev.org"];
    aCount.value = scopes.length;
    return scopes;
  },

  // xeIDocumentPack
  get initialAppPath() {
    this.checkInitialized();
    assert(this._appPath.indexOf("..") == -1, "What happened?");
    return this._appPath;
  },

  // xeIDocumentPack
  initializeByPrecondition: NOT_IMPLEMENTED,

  // xeIDocumentPack
  initializeByFile: function initializeByFile(aLocalPath) {
    // Fix up the URL.
    var path = aLocalPath;
    const quoteChar = (path.indexOf("/") == -1) ? "\\" : "/";
    const upOneLevel = quoteChar + ".." + quoteChar;
    while (path.indexOf(upOneLevel) != -1) {
      var firstPart = path.substr(0, path.indexOf(upOneLevel));
      firstPart = firstPart.substr(0, firstPart.lastIndexOf(quoteChar) + 1);
      var secondPart = path.substr(path.indexOf(upOneLevel) + 4);
      path = firstPart + secondPart;
    }
    this._appPath = path;
    var index = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
    this._baseDirPath = path.substr(0, index + 1);

    // Search for chrome.manifest; we need to determine default locales, skins
    var engine = Components.classes["@verbosio.mozdev.org/file-search/engine;1"]
                           .createInstance(xeIFileSearchEngine);

    var criteria = engine.getNewCriteria();
    criteria.filterPropertyString("isFile", "true");
    criteria.filterPropertyString("leaf", "chrome.manifest");

    // nsISimpleEnumerator
    var enumerator = engine.searchDirectory(this._baseDirPath, true);
    var manifests = [];

    while (enumerator.hasMoreElements()) {
      var result = enumerator.getNext();
      assert(result instanceof xeIFileSearchResult, "Not a search result?");
      manifests[manifests.length] = result.path;
    }
    manifests.sort(); // Short ones before longer ones, alphabetical sorting

    for (var i = 0; i < manifests.length; i++) {
      this.processManifest(manifests[i]);
    }

    this.txlistener = null;

    this._initialized = true;
  },

  /**
   * Search a set of base URI maps for a matching base URI.
   *
   * @param aPath Path to match a URI to.
   * @param aMapSet JavaScript object with base URI maps as properties.
   *
   * @return xeIBaseURIMap matching the path best.
   */
  searchURIMaps: function searchURIMaps(aPath, aMapSet) {
    var currentMap = null;
    var currentURI = "";
    for (var i in aMapSet) {
      map = aMapSet[i];
      assert(map instanceof xeIBaseURIMap, "Not a base URI map?");
      var types = map.getURITypes({});
      for (var i = 0; i < types.length; i++) {
        var currentType = types[i];
        var uri = map.getURI(currentType);
        if (!uri) {
          ERROR_FAILURE();
        }
        if ((aPath.indexOf(uri) == 0) && (uri.length > currentURI.length)) {
          currentMap = map;
          currentURI = uri;
        }
      }
    }
    return currentMap;
  },

  // xeIDocumentPack
  getBaseURI: function getBaseURI(aPath) {
    this.checkInitialized();
    var currentMap = null;
    var currentURI = "";
    var prop;
    var path = aPath;

    // Fix up the URL.
    const quoteChar = (path.indexOf("/") == -1) ? "\\" : "/";
    const upOneLevel = quoteChar + ".." + quoteChar;
    while (path.indexOf(upOneLevel) != -1) {
      var firstPart = path.substr(0, path.indexOf(upOneLevel));
      firstPart = firstPart.substr(0, firstPart.lastIndexOf(quoteChar) + 1);
      var secondPart = path.substr(path.indexOf(upOneLevel) + 4);
      path = firstPart + secondPart;
    }

    if (path.indexOf("file://") == 0) {
      path = fileProtocol.getFileFromURLSpec(path).path;
    }

    // content chrome URI's
    currentMap = this.searchURIMaps(path, this._contentURIMaps);

    // locale chrome URI's
    if (!currentMap) {
      if (this._selectedLocale in this._localeURIMaps) {
        var localeMap = this._localeURIMaps[this._selectedLocale];
        currentMap = this.searchURIMaps(path, localeMap);
      }
    }
    if (!currentMap) {
      for (prop in this._localeURIMaps) {
        if (prop == this._selectedLocale) {
          // We've already checked it, move on.
          continue;
        }
        currentMap = this.searchURIMaps(path, this._localeURIMaps[prop]);
        if (currentMap) {
          break;
        }
      }
    }

    // skin chrome URI's
    if (!currentMap) {
      if (this._selectedSkin in this._skinURIMaps) {
        var skinMap = this._skinURIMaps[this._selectedSkin];
        currentMap = this.searchURIMaps(path, skinMap);
      }
    }
    if (!currentMap) {
      for (prop in this._skinURIMaps) {
        if (prop == this._selectedSkin) {
          // We've already checked it, move on.
          continue;
        }
        currentMap = this.searchURIMaps(path, this._skinURIMaps[prop]);
        if (currentMap) {
          break;
        }
      }
    }

    // any remaining URI's, including components files
    if (!currentMap) {
      currentMap = this.searchURIMaps(path, this._baseURIMaps);
    }

    if (currentMap) {
      assert(currentMap instanceof xeIBaseURIMap, "Check current map!");
      return currentMap;
    }

    // Okay, we need to make a new base URI map.
    currentMap = Components.classes["@verbosio.mozdev.org/base-uri-map;1"]
                           .createInstance(xeIBaseURIMap);
    var slash = (this._appPath.indexOf("/") == -1) ? "\\" : "/";
    var chromePath;
    var localPath;
    if (path.indexOf("chrome://") == 0) {
      // Chrome file.
      if ((path.indexOf("chrome://global/") == 0) ||
          (path.indexOf("chrome://toolkit/") == 0)) {
        // We don't support those.
        return null;
      }
      dump(path + "\n");
      ERROR_ABORT(); // this should have been handled already!
    } else

    if (path.indexOf("resource://") == 0) {
      NOT_IMPLEMENTED();
    } else

    // By now, the path should be local.
    if (path.indexOf("://") != -1) {
      // Note:  this NOT_IMPLEMENTED is real.
      NOT_IMPLEMENTED();
    }

    // Find the directory having an application.ini or install.rdf above this.
    var engine = Components.classes["@verbosio.mozdev.org/file-search/engine;1"]
                           .createInstance(xeIFileSearchEngine);
    var criteria = [];

    criteria[0] = engine.getNewCriteria();
    criteria[0].filterPropertyString("isFile", "true");
    criteria[0].filterPropertyString("leaf", "application.ini");

    criteria[1] = engine.getNewCriteria();
    criteria[1].filterPropertyString("isFile", "true");
    criteria[1].filterPropertyString("leaf", "install.rdf");

    // nsISimpleEnumerator
    var enumerator = engine.searchDirectory(this._baseDirPath, true);
    var bestPath = "";

    while (enumerator.hasMoreElements()) {
      var result = enumerator.getNext();
      assert(result instanceof xeIFileSearchResult, "Not a search result?");
      var index = Math.max(result.path.lastIndexOf("/"),
                           result.path.lastIndexOf("\\"));
      currentURI = result.path.substr(0, index + 1);

      dump(currentURI + "\n");
      if ((path.indexOf(currentURI) == 0) &&
          (currentURI.length > bestPath.length)) {
        bestPath = currentURI;
      }
    }
    assert(bestPath, "No best path for " + path + "???");
    currentMap.addType("local", bestPath);

    // leak cleanup?
    fileSearch = null;
    criteria = null;
    enumerator = null;
    result = null;

    this._baseURIMaps[this._baseURIMaps.length] = currentMap;
    return currentMap;
  },

  /**
   * Find the wrapper in our own registry matching a given path.
   *
   * @param aPath The path to the file the wrapper holds.
   *
   * @return xeIDocumentWrapper if found, or null if not found.
   */
  findWrapper: function findWrapper(aPath) {
    this.checkInitialized();
    var path = this.completeURI(aPath);
    var baseURI = this.getBaseURI(path);
    var path = baseURI.getPrimaryLocalURI(path);
    var wrapper;
    var localURI = baseURI.getURI("local");
    var i;

    for (i = 0; i < this.length; i++) {
      wrapper = this.item(i);
      try {
        if (wrapper.baseURIMap.getURI("local") == localURI &&
            wrapper.pathToDocument == path.substr(localURI.length)) {
          return wrapper;
        }
      } catch (e) {
        dump(i + " of " + this.length + "\n\n");
        dump(e.stack + "\n\n");
        throw e;
      };
    }
    return null;
  },

  // xeIDocumentPack
  hasWrapper: function hasWrapper(aPath) {
    return Boolean(this.findWrapper(aPath));
  },

  // xeIDocumentPack
  getWrapper: function getWrapper(aPath) {
    var path = this.completeURI(aPath);
    var wrapper = this.findWrapper(path);
    if (wrapper) {
      return wrapper;
    }

    var extension = path.substr(path.lastIndexOf(".") + 1);
    /* We don't try to catch an error here; the user-interface should have
       already verified the category, and thus the component, exists.
     */
    var classId = catMan.getCategoryEntry(catName, extension);
    wrapper = Components.classes[classId]
                        .createInstance(xeIDocumentWrapper);
    var baseURI = this.getBaseURI(path);
    var path = baseURI.getPrimaryLocalURI(path);
    wrapper.initializeByFile(path, this);
    this.addWrapper(wrapper);
    return wrapper;
  },

  // nsIClassInfo
  getInterfaces: function getInterfaces(aCount) {
    var interfaces = [xeIChromeDocumentPack,
                      xeIDocumentPack,
                      nsIClassInfo];
    aCount.value = interfaces.length;
    return interfaces;
  },

  // nsIClassInfo
  getHelperForLanguage: function getHelperForLanguage(aLanguage) {
    return null;
  },

  // nsIClassInfo
  implementationLanguage: nsIProgrammingLanguage.JAVASCRIPT,

  // nsIClassInfo
  flags: 0,

  // nsISupports
  QueryInterface: function QueryInterface(aIID) {
    if (aIID.equals(xeIChromeDocumentPack) ||
        aIID.equals(xeIDocumentPack) ||
        aIID.equals(nsIClassInfo) ||
        aIID.equals(nsISupports))
      return this;

    // Known interfaces we don't support
    if (aIID.equals(nsIDOMWindow) ||
        aIID.equals(nsISecurityCheckedComponent)) {
      throw Components.results.NS_ERROR_NO_INTERFACE;
      return null;
    }

    // Discover what interfaces we need
    var i = Components.interfaces;
    var found = false;
    for (var prop in i) {
      if (aIID.equals(i[prop])) {
        found = true;
        dump("QUERYINTERFACE DocumentPack: " + prop + "\n");
        break;
      }
    }
    if (!found) {
      dump("QUERYINTERFACE DocumentPack: Unknown\n");
    }

    throw Components.results.NS_ERROR_NO_INTERFACE;
    return null;
  }
};

function AppDocumentPack() {
  this.startup();
}
AppDocumentPack.prototype = new DocumentPack();
AppDocumentPack.prototype.addProperties({
  // nsIClassInfo
  classDescription: "XUL application document pack",

  // nsIClassInfo
  classID: Components.ID("{3bec7b44-364b-402f-a57f-e430e4dc3453}"),

  // nsIClassInfo
  contractID: "@verbosio.mozdev.org/document-pack;1?type=xul-application"
});

Module.addConstructor(AppDocumentPack);

function InstallRDFDocPack() {
  this.startup();
}
InstallRDFDocPack.prototype = new DocumentPack();
InstallRDFDocPack.prototype.addProperties({
  // nsIClassInfo
  classDescription: "XUL extension document pack",

  // nsIClassInfo
  classID: Components.ID("{dea7d63b-8b18-4ba2-9b1c-c58516469e88}"),

  // nsIClassInfo
  contractID: "@verbosio.mozdev.org/document-pack;1?type=xul-install-rdf"
});

Module.addConstructor(InstallRDFDocPack);
