/* ***** 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 Verbosio Utility Component Service.
 *
 * 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 ***** */

// Get JS modules.
Components.utils.import("resource://app/modules/XPCOMUtils.jsm");
Components.utils.import("resource://app/modules/ArrayConverter.jsm");
Components.utils.import("resource://app/modules/ecma-debug.jsm");
Components.utils.import("resource://app/modules/XMLValidator.jsm");

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

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

var C_i = Components.interfaces;

const xeIVerbosioUtilsService     = C_i.xeIVerbosioUtilsService;
const xeIXMLDocumentWrapper       = C_i.xeIXMLDocumentWrapper;
const xeITextDocumentWrapper      = C_i.xeITextDocumentWrapper;
const xeIXMLDocumentFile          = C_i.xeIXMLDocumentFile;
const nsIXMLHttpRequest           = C_i.nsIXMLHttpRequest;
const nsIDOMSerializer            = C_i.nsIDOMSerializer;
const nsISAXXMLReader             = C_i.nsISAXXMLReader;
const nsITransferable             = C_i.nsITransferable;
const nsIClipboard                = C_i.nsIClipboard;
const nsIClipboardOwner           = C_i.nsIClipboardOwner;
const nsIFileProtocolHandler      = C_i.nsIFileProtocolHandler;
const nsIExtensionManager         = C_i.nsIExtensionManager;
const nsIUpdateItem               = C_i.nsIUpdateItem;
const nsISupportsString           = C_i.nsISupportsString;
const mozIJSSubScriptLoader       = C_i.mozIJSSubScriptLoader;
const nsIScriptError              = C_i.nsIScriptError;
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 docFileClass    = "@verbosio.mozdev.org/document-file;1?"
                      + "contentType=application/xml";
const serializerClass = "@mozilla.org/xmlextras/xmlserializer;1";

const fileProtocol  = Components.classes[fileClass]
                                .getService(nsIFileProtocolHandler);
const clipboard     = Components.classes["@mozilla.org/widget/clipboard;1"]
                                .getService(nsIClipboard);
const serializer    = Components.classes[serializerClass]
                                .createInstance(nsIDOMSerializer);

var subscriptsLoaded = false;
function loadSubscripts() {
  if (!subscriptsLoaded) {
    var jsLoader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
                             .getService(mozIJSSubScriptLoader);
    jsLoader.loadSubScript("chrome://jslib/content/jslib.js");
    jsLoader.loadSubScript("chrome://jslib/content/io/io.js");
    subscriptsLoaded = true;
  }
}

function getMinIndexOfString(aString, aOffset, aChar) {
  var index = aString.indexOf(aChar, aOffset);
  return (index == -1) ? Infinity : index;
}

const clipboardOwner = {
  clipboardObject: null,
  contentType: null,

  // nsIClipboardOwner
  LosingOwnership: function LosingOwnership(aTransferable) {
    this.clipboardObject = null;
    this.contentType = null;
  },

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

    // 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 clipboardOwner: " + prop + "\n");
        break;
      }
    }
    if (!found) {
      dump("QUERYINTERFACE clipboardOwner: Unknown\n");
    }

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

const CLIP_NODE = "application/x-vnd.verbosio.dom-node";
const MARKUP_NS = "http://verbosio.mozdev.org/namespaces/markup/";
const LANG_NS   = "http://verbosio.mozdev.org/namespaces/language/";

/* languageTemplatesMap[namespaceURI] = (xeIXMLDocumentFile) */
var languageTemplatesMap = null;

/* markupTemplates[namespaceURI][localName] = [
    (xeIMarkupTemplate), (xeIMarkupTemplate), ...
  ]
*/
var markupTemplatesMap = {};

xeVerbosioUtils = {
  _extensionFiles: null,

  // xeIVerbosioUtils
  getDTDFragment: function getDTDFragment(aSource) {
    var aStartIndex = aSource.indexOf("<!DOCTYPE");
    if (aStartIndex == -1) {
      return null;
    }

    var charIndex = aStartIndex;
    var firstGT = getMinIndexOfString(aSource, charIndex, ">");
    var openBracket = getMinIndexOfString(aSource, charIndex, "[");

    // Find the end of the tag.  Doctypes can contain internal entities.
    if (openBracket < firstGT) {
      var firstLT = getMinIndexOfString(aSource, openBracket, "<");
      var closeBracket = getMinIndexOfString(aSource, openBracket, "]");
      charIndex = firstLT;

      while (firstLT < closeBracket) {
        // We have a tag within the internal subset.  It may end at firstGT.

        var firstSingleQuote = getMinIndexOfString(aSource, charIndex, "'");
        var firstDoubleQuote = getMinIndexOfString(aSource, charIndex, '"');
        var firstCommentOpen = getMinIndexOfString(aSource, charIndex, "<!--");

        // Skip comments.  They're not supposed to be in entities anyway.
        if (firstCommentOpen < firstGT) {
          charIndex = getMinIndexOfString(aSource, firstCommentOpen + 4, "-->") + 4;
        } else

        // Speed past pairs of quotes.
        if (firstSingleQuote < firstGT) {
          charIndex = getMinIndexOfString(aSource, firstSingleQuote + 1, "'") + 2;
        } else if (firstDoubleQuote < firstGT) {
          charIndex = getMinIndexOfString(aSource, firstDoubleQuote + 1, '"') + 2;
        } else {
          charIndex = firstLT + 1;
        }

        // Since we referred to firstGT, recalculate that too.
        firstGT = getMinIndexOfString(aSource, charIndex, ">");

        // Reset parameters for loop.
        firstLT = getMinIndexOfString(aSource, charIndex, "<");
        closeBracket = getMinIndexOfString(aSource, charIndex, "]");
      }

      // The close bracket has shown up, so the internal set closes there.
    } // end if (openBracket < firstGT)

    firstGT = getMinIndexOfString(aSource, closeBracket, ">");
    var doctype = aSource.substring(aStartIndex, firstGT + 1);
    return doctype;
  },

  // xeIVerbosioUtilsService
  copySelection: function copySelection(aWrapper) {
    var trans = Components.classes["@mozilla.org/widget/transferable;1"]
                          .createInstance(nsITransferable);
    var contents;
    var str;
    if (aWrapper instanceof xeIXMLDocumentWrapper) {
      var range = aWrapper.selectionRange;
      var fragment = aWrapper.selectionRange.cloneContents();

      trans.addDataFlavor(CLIP_NODE);

      contents = serializer.serializeToString(fragment, "application/xml");
      str      = Components.classes["@mozilla.org/supports-string;1"]
                           .createInstance(nsISupportsString);
      str.data = contents;

      trans.setTransferData(CLIP_NODE,
                            str,
                            contents.length * 2);
      trans.addDataFlavor("text/unicode");
      trans.setTransferData("text/unicode", str, contents.length * 2);
      clipboard.setData(trans, clipboardOwner, nsIClipboard.kGlobalClipboard);
      clipboardOwner.clipboardObject = fragment;
      clipboardOwner.contentType = CLIP_NODE;
      return;
    }

    if (aWrapper instanceof xeITextDocumentWrapper) {
      contents = aWrapper.wrappedSource;
      contents = contents.substring(aWrapper.selectionStartIndex,
                                    aWrapper.selectionEndIndex);
      str      = Components.classes["@mozilla.org/supports-string;1"]
                           .createInstance(nsISupportsString);
      str.data = contents;
      trans.addDataFlavor("text/unicode");
      trans.setTransferData("text/unicode", str, contents.length * 2);
      clipboard.setData(trans, clipboardOwner, nsIClipboard.kGlobalClipboard);
      clipboardOwner.clipboardObject = null;
      clipboardOwner.contentType = null;
      return;
    }

    NOT_IMPLEMENTED();
  },

  // xeIVerbosioUtilsService
  get clipboardType() {
    return clipboardOwner.contentType;
  },

  // xeIVerbosioUtilsService
  getClipboardContents: function getClipboardContents(aFlavor, aStoredObject) {
    var trans = Components.classes["@mozilla.org/widget/transferable;1"]
                          .createInstance(nsITransferable);
    trans.addDataFlavor(aFlavor);
    const clipboard = Components.classes["@mozilla.org/widget/clipboard;1"]
                                .getService(nsIClipboard);
    clipboard.getData(trans, nsIClipboard.kGlobalClipboard);
    var contents = {};
    try {
      trans.getTransferData(aFlavor, contents, {});
    } catch (e) {
      aStoredObject.value = null;
      return null;
    }
    aStoredObject.value = (clipboardOwner.contentType == aFlavor) ?
                          clipboardOwner.clipboardObject :
                          null;
    return contents.value.QueryInterface(nsISupportsString).data;
  },

  // xeIVerbosioUtilsService
  getExtensionDataFiles: function getExtensionDataFiles(aCount) {
    if (this._extensionFiles === null) {
      try {
        // Go get them!
        this._extensionFiles = [];
        var itemCount = {};
        const extmgr = Components.classes["@mozilla.org/extensions/manager;1"]
                                 .getService(nsIExtensionManager);
        var itemList = extmgr.getItemList(nsIUpdateItem.TYPE_EXTENSION,
                                          itemCount);
        var rv = [];

        for (var i = 0; i < itemCount.value; i++) {
          var item = itemList[i];
          var loc = extmgr.getInstallLocation(item.id);
          var file = loc.getItemFile(item.id, "verbosio-data.xml");

          if (!file.exists()) {
            continue;
          }

          try {
            file.normalize();
          } catch (e) {
            if (e.stack) {
              dump(e.stack);
            } else {
              dumpJSStack();
            }
            dump("\n\n");
            throw e;
          }
          dump(file.path + "\n");

          var docFileObj = Components.classes[docFileClass]
                                     .createInstance(xeIXMLDocumentFile);
          docFileObj.init(file.path, "application/xml");
          docFileObj.read();
          docFileObj.parse({
            subsetWrapper: null,
            documentType: null
          }, null);
          assert(docFileObj.contentDocument, "No XML document?");
          this._extensionFiles[this._extensionFiles.length] = docFileObj;
        }
      } catch (e) {
        // We suck.
        this._extensionFiles = null;
        throw e;
      }
    }

    var rv = [];
    for (var i = 0; i < this._extensionFiles.length; i++) {
      rv[i] = this._extensionFiles[i];
    }
    aCount.value = rv.length;
    return rv;
  },

  // xeIVerbosioUtilsService
  getTemplateDocument: function getTemplateDocument(aNamespaceURI) {
    if (!aNamespaceURI) {
      throw Components.results.NS_ERROR_INVALID_ARG;
    }

    if (!languageTemplatesMap) {
      this.getExtensionDataFiles({});
      assert(this._extensionFiles.length > 0, "We're missing extensions.");
      languageTemplatesMap = {};

      for (var i = 0; i < this._extensionFiles.length; i++) {
        var docPath = this._extensionFiles[i].location;
        var doc = this._extensionFiles[i].contentDocument;
        var langElements = doc.getElementsByTagNameNS(LANG_NS,
                                                      "language");
        if (!langElements) {
          dump("No <markup:language/> elements found: " + docPath + "\n");
          continue;
        }
        var lang = langElements[0];
        if (!lang) {
          dump("No <markup:language/> elements found: " + docPath + "\n");
          continue;
        }
        if (lang.getAttribute("type") != "xml-markup") {
          dump("lang.getAttribute('type'): '" + lang.getAttribute('type') + "'\n");
          continue;
        }

        nsURI = lang.getAttribute("namespaceURI");
        if (!nsURI) {
          dump("Missing namespace URI in extension file: " + docPath);
          continue;
        }

        var chromeName = lang.getAttribute("chrome-name");
        if (!chromeName) {
          dump("Missing chrome name in <markup:language/>: " + docPath);
          continue;
        }

        var chromeURL = "chrome://" + chromeName + "/content/templates.xml";
        dump("Checking for " + chromeURL + "\n");
        loadSubscripts();
        var chromeFile = new ChromeFile(chromeURL);
        if (!chromeFile.exists()) {
          dump("Chrome file not found.\n");
          continue;
        }
        if (chromeFile.isJarFile) {
          dump("Chrome file is a jarred file; we can't process that.\n");
          continue;
        }

        // SAXReader sanity check
        try {
          chromeFile.open();
          XMLValidator.validate(chromeFile.read());
          chromeFile.close();
        } catch (e) {
          chromeFile.close();

          // Some kind of syntax error.  We can't create the document.
          dump(XMLValidator.locator.lineNumber + "@");
          dump(XMLValidator.locator.columnNumber + "\n");
          dump(XMLValidator.error + "\n\n");
          continue;
        }

        // Fetch the XML document.
        var req = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
                            .createInstance(nsIXMLHttpRequest);
        req.open("GET",
                 chromeURL,
                 false);
        req.send(null);
        var templateDoc = req.responseXML;
        assert(templateDoc, "The file exists, but is not well-formed XML?");
        dump(chromeURL + " found and well-formed\n");

        var docRoot = templateDoc.documentElement;
        // Root element sanity check.
        if ((docRoot.namespaceURI != MARKUP_NS) ||
            (docRoot.localName != "templates")) {
          dump("Root element isn't <markup:templates/>: " + chromeURL);
          continue;
        }

        // Add the document to the language template map.
        dump("Setting languageTemplatesMap[" + nsURI + "] to ");
        dump(templateDoc + "\n");
        languageTemplatesMap[nsURI] = templateDoc;
      } // end for (var i = 0; i < this._extensionFiles.length; i++)
    }

    dump("aNamespaceURI in getTemplateDocument: " + aNamespaceURI + "\n");

    return (aNamespaceURI in languageTemplatesMap) ?
           languageTemplatesMap[aNamespaceURI] :
           null;
  },

  // xeIVerbosioUtilsService
  getSupportingTemplates: function getSupportingTemplates(aNode, aCount) {
    if (!aNode) {
      throw Components.results.NS_ERROR_INVALID_ARG;
    }
    var nsURI = aNode.namespaceURI;
    var localName;
    var nsMap;
    var templates;

    aValues = [];
    aCount.__defineGetter__("value", function() {
      return aValues.length;
    });

    if (!(aNode.namespaceURI in markupTemplatesMap) ||
        !(aNode.localName in markupTemplatesMap[aNode.namespaceURI])) {
      var templateDoc = this.getTemplateDocument(aNode.namespaceURI);
      if (!templateDoc) {
        dump("No template doc found for aNode's namespace URI.\n");
        return aValues;
      }

      // Check the templates.
      templates = templateDoc.getElementsByTagNameNS(MARKUP_NS, "template");
      if (templates.length == 0) {
        dump("No <markup:template/> elements: " + chromeURL);
        return aValues;
      }
      for (var j = 0; j < templates.length; j++) {
        if (!templates[j].hasAttribute("localName")) {
          dump("<markup:template/> element missing localname attribute: " +
               chromeURL);
          return aValues;
        }
      }
      assert(nsURI, "No namespace URI?");

      // Finally, process the templates!
      dump("Processing templates: " + nsURI + "\n");
      if (!(nsURI in markupTemplatesMap)) {
        dump("Adding '" + nsURI + "' to markupTemplatesMap\n");
        markupTemplatesMap[nsURI] = {};
      }
      nsMap = markupTemplatesMap[nsURI];
      for (j = 0; j < templates.length; j++) {
        localName = templates[j].getAttribute("localName");
        dump("localName " + localName + ": " + templates[j] + "\n");
        if (!(localName in nsMap)) {
          nsMap[localName] = [];
        }
        nsMap[localName].push(templates[j]);
      }
    } // end search for markupTemplatesMap[namespaceURI][localName];

    // Look up the node in the markup templates map.
    if (!(nsURI in markupTemplatesMap)) {
      dump("'" + nsURI + "' not found in markupTemplatesMap\n");
      return aValues;
    }

    nsMap = markupTemplatesMap[nsURI];
    localName = aNode.localName;
    if (!(localName in nsMap)) {
      dump(localName + " not found in markupTemplatesMap[nsURI]\n");
      return aValues;
    }

    templates = nsMap[localName];
    // Sort the templates, and remove ones that don't count.
    var supportRatings = [];
    for (i = 0; i < templates.length; i++) {
      // First, copy them.  We never return our native templates map.
      aValues[i] = templates[i];
      supportRatings[i] = aValues[i].rateSupport(aNode);
    }

    for (i = supportRatings.length - 1; i >= 0; i--) {
      if (supportRatings[i] == 0) {
        aValues.splice(i, 1);
        supportRatings.splice(i, 1);
      }
    }
    if (aValues.length == 0) {
      dump("Returning empty set of values?  Nothing supported?");
    }

    aValues.sort(function sortTemplates(a, b) {
      // Find the index numbers.
      for (i = 0; i < aValues.length; i++) {
        assert(aValues[i] > 0, "We missed one?");
        if (aValues[i] == a) {
          break;
        }
      }
      assert(i < aValues.length, "What happened?");

      for (j = 0; j < aValues.length; j++) {
        assert(aValues[j] > 0, "We missed one?");
        if (aValues[j] == b) {
          break;
        }
      }
      assert(j < aValues.length, "What happened?");
      return supportRatings[i] - supportRatings[j];
    });

    return aValues;
  },

  _interfaces: [xeIVerbosioUtilsService,
                nsIClassInfo],

  // nsIClassInfo
  getInterfaces: ArrayConverter.xpcomArrayMethod("_interfaces"),

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

  // nsIClassInfo
  classDescription: "xeIVerbosioUtils service",

  // nsIClassInfo
  classID: Components.ID("{9fa2e010-84b4-4537-bd29-a2795c1c699d}"),

  // nsIClassInfo
  contractID: "@verbosio.mozdev.org/verbosio-utils;1",

  // nsIClassInfo
  implementationLanguage: nsIProgrammingLanguage.JAVASCRIPT,

  // nsIClassInfo
  flags: nsIClassInfo.SINGLETON,

  // nsISupports
  QueryInterface: function QueryInterface(aIID) {
    for (var j = 0; j < this._interfaces.length; j++) {
      if (aIID.equals(this._interfaces[j])) {
        return this;
      }
    }

    if (aIID.equals(nsISupports))
      return this;

    // Known interfaces we don't support
    if (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 xeVerbosioUtils: " + prop + "\n");
        break;
      }
    }
    if (!found) {
      dump("QUERYINTERFACE xeVerbosioUtils: Unknown\n");
    }

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

var NSGetModule = XPCOMUtils.generateNSGetModule([
  wrapServiceComponent(xeVerbosioUtils)
], null, null);
