// BEGIN FLOCK GPL
//
// Copyright Flock Inc. 2005-2009
// http://flock.com
//
// This file may be used under the terms of the
// GNU General Public License Version 2 or later (the "GPL"),
// http://www.gnu.org/licenses/gpl.html
//
// 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.
//
// END FLOCK GPL

const CC = Components.classes;
const CI = Components.interfaces;
const CR = Components.results;
const CU = Components.utils;

CU.import("resource:///modules/FlockStringBundleHelpers.jsm");
CU.import("resource:///modules/FlockXPCOMUtils.jsm");
FlockXPCOMUtils.debug = false;

const MODULE_NAME = "Flock Rich Drag and Drop Service";

// The Rich Drag&Drop service
const FLOCK_RICHDRAGDROP_SERVICE_CLASS_NAME
        = "Flock Rich Drag and Drop Service";
const FLOCK_RICHDRAGDROP_SERVICE_CLASS_ID
        = Components.ID("{30b8dc92-abe0-44ee-a845-e1a827d59610}");
const FLOCK_RICHDRAGDROP_SERVICE_CONTRACT_ID = "@flock.com/rich-dnd-service;1";
const FLOCK_PREF_SERVICE_CONTRACT_ID = "@mozilla.org/preferences-service;1";
const FLOCK_CATEGORY_MANAGER_CONTRACT_ID = "@mozilla.org/categorymanager;1";
const FLOCK_RICH_CONTENT_CATEGORY = "flockRichContentHandler";
const FLOCK_BREADCRUMB_ID = "flock-breadcrumb";

const FLAVOR_UNICODE = "text/unicode";
const FLAVOR_HTML = "text/html";
const FLAVOR_MOZ_URL = "text/x-moz-url";
const FLAVOR_FLOCK_MEDIA = "text/x-flock-media";
const FLAVOR_TEXT_CITE = "text/x-flock-textcitation";
const FLAVOR_HTML_CITE = "text/x-flock-htmlcitation";
const FLAVOR_MOZ_RDF = "moz/rdfitem";

// From viewPartialSource.js
//
// These are markers used to delimit the selection during processing. They
// are removed from the final rendering, but we pick space-like characters for
// safety (and futhermore, these are known to be mapped to a 0-length string
// in transliterate.properties). It is okay to set start=end, we use findNext()
// U+200B ZERO WIDTH SPACE
const MARK_SELECTION_START = "\u200B\u200B\u200B\u200B\u200B";
const MARK_SELECTION_END = "\u200B\u200B\u200B\u200B\u200B";

const NS_XHTML = "http://www.w3.org/1999/xhtml";

/**************************************************************************
 * Component: Flock Rich Drag and Drop Service
 **************************************************************************/

// Constructor.
function flockRichDragDropService() {
  this._logger = CC["@flock.com/logger;1"].createInstance(CI.flockILogger);
  this._logger.init("flockRichDragDropService");
}

/**************************************************************************
 * Flock Rich Drag and Drop Service: XPCOM Component Creation
 **************************************************************************/

flockRichDragDropService.prototype = new FlockXPCOMUtils.genericComponent(
  FLOCK_RICHDRAGDROP_SERVICE_CLASS_NAME,
  FLOCK_RICHDRAGDROP_SERVICE_CLASS_ID,
  FLOCK_RICHDRAGDROP_SERVICE_CONTRACT_ID,
  flockRichDragDropService,
  CI.nsIClassInfo.SINGLETON,
  [
    CI.flockIRichDNDService
  ]
);

// FlockXPCOMUtils.genericComponent() registration callbacks.
flockRichDragDropService.prototype.register =
function flockRichDragDropService_register(aCompMgr, aFileSpec, aLocation, aType) {
}

flockRichDragDropService.prototype.unregister =
function flockRichDragDropService_unregister(aCompMgr, aLocation, aType) {
}


/**************************************************************************
 * Flock Rich Drag and Drop Service: flockIRichDNDService Implementation
 **************************************************************************/


// Add the content to the targeted element in the rich text editor
flockRichDragDropService.prototype.addToRichTextEditor =
function fRDDS_addToRichTextEditor(aContent, aTargetElement) {
  if (aContent && aTargetElement) {
    var doc = aTargetElement.ownerDocument;
    // Place the elements
    var targetName = aTargetElement.localName.toLowerCase();
    if (targetName == "html" || targetName == "body") {
      // We cannot identify the specific element upon which we've dropped our
      // content, so add it at insertion point
      var content = "<span>" + aContent + "</span>";
      if (!this._documentHasBreadcrumb(doc)) {
        // Add breadcrumb after content
        content += '<span id="' + FLOCK_BREADCRUMB_ID + '">'
                + this.getBreadcrumb("rich")
                + "</span>";
      }
      // Add content at insertion point
      doc.execCommand("insertHTML", false, content);
    } else {
      // We've dropped anto element within the body, so insert our content
      // after this element, i.e. before its next sibling.
      var parent = aTargetElement.parentNode;
      var insertBeforeNode = aTargetElement.nextSibling;
      // Be careful not to drop on the breadcrumb
      if (this.hasBreadcrumb(aTargetElement)) {
        // Add before the breadcrumb instead of after it
        insertBeforeNode = aTargetElement;        
      } else if (parent.localName.toLowerCase() == "a") {
        // Also be careful not to drop within a link -- add it after the link
        insertBeforeNode = parent.nextSibling;
        parent = parent.parentNode;
      }
      // Create SPAN element containing content
      var contentElement = doc.createElement("span");
      contentElement.innerHTML = aContent;
      // Add content after target
      parent.insertBefore(contentElement, insertBeforeNode);
      // Check for breadcrumb -- we don't want to add more than one
      if (!this._documentHasBreadcrumb(doc)) {
        // Build the breadcrumb element
        var breadcrumbElement = doc.createElement("span");
        breadcrumbElement.setAttribute("id", FLOCK_BREADCRUMB_ID);
        breadcrumbElement.innerHTML = this.getBreadcrumb("rich");
        // Add breadcrumb after content
        parent.insertBefore(breadcrumbElement, insertBeforeNode);
      }
    }
  }
};

// nsITransferable getRichSelection(in nsIDragSession aSession,
//                                  in AString aDropFlavour,
//                                  in AString aDropData);
flockRichDragDropService.prototype.getRichSelection =
function fRDDS_getRichSelection(aSession, aDropFlavour, aDropData) {
  var node = aSession.sourceNode;
  // Don't handle items dragged from outside the browser 
  if (!node) {
    return null;
  }

  // What kind of node are we dragging?
  var nodeName = node.nodeName.toLowerCase();
  this._logger.debug("nodeName = "+nodeName);

  // Browser tab: to be sent as a link
  if ((nodeName == "tab") || (nodeName == "xul:tab")) {
    return this._getLinkTransferable(node.linkedBrowser.currentURI.spec,
                                     node.label);
  }

  // Bookmark item: to be sent as a link
  if ((nodeName == "toolbarbutton" || nodeName == "menuitem") &&
      node.className.indexOf("bookmark-item") > -1)
  {
    return this._getLinkTransferable(node.node.uri, node.label);
  }

  // Mediabar
  if ((nodeName == "photo") || (nodeName == "xul:photo")) {
    return this._getMediabarTransferable(node);
  }

  // Web clipboard
  if (nodeName == "shelficon") {
    return this._getWebClipboardTransferable(aDropFlavour, aDropData);
  }

  // Do not support drags from these multiple-node tree objects 
  if ((nodeName == "richtreefolder") || (nodeName == "treechildren")) {
    return null;
  }

  // Gather some information about the window from which the node was dragged,
  // as it will be used to enrich the data we will be sending
  var top = node.ownerDocument.defaultView.top;
  var doc = top.document;
  var topUrl = doc.location.toString();
  if (topUrl && topUrl.match(/^chrome:\/\/.+/)) {
    // Never send content with a chrome URL
    if (doc.defaultView.getBrowser) {
      // The chrome content is related to the page in the current browser tab
      topUrl = doc.defaultView.getBrowser().currentURI.spec;
    } else {
      // There is no URL to share
      topUrl = "";
    }
  }
  // Get title from browser rather than window, to avoid extra text tacked on
  // by the OS
  var topTitle = doc.defaultView.getBrowser
               ? doc.defaultView.getBrowser().contentTitle
               : doc.title;
  if (!topTitle) {
    topTitle = topUrl;
  }

  // The URL bar favicon: appear as a XUL image but must be treated as a link
  if ((nodeName == "image") || (nodeName == "xul:image")) {
    return this._getLinkTransferable(topUrl, topTitle);
  }

  // HTML image from a web page
  if (nodeName == "img") {
    var imageTitle = "";
    if (node.title) {
      imageTitle = node.title;
    } else if (node.alt) {
      imageTitle = node.alt;
    }
    var imageURL = this._makeURLAbsolute(topUrl, node.src);
    // Don't insert a stupid image containing Javascript
    if (imageURL.match("javascript:")) {
      return null;
    }
    if (imageURL == topUrl) {
      // Bug 3756, when the image alone is displayed in the browser
      // Mozilla is replacing the "alt" tag by "This image contains error"
      // a clever hack, but we don't want to use that for the title
      imageTitle = imageURL.split("/").pop();
    }

    return this._getImageTransferable(imageURL, imageTitle, topUrl);
  }

  // HTML link from an editor -- don't want to use the selection
  if (nodeName == "a") {
    if (node.href) {
      var title = node.textContent ? node.textContent : node.title;
      return this._getLinkTransferable(node.href, title);
    }
  }

  // Handle drags from MyWorld
  if (topUrl == "about:myworld" && aDropFlavour && aDropData) {
    // MyWorld drop data is usually text/x-moz-url
    switch (aDropFlavour) {
      case FLAVOR_MOZ_URL:
        var data = aDropData.split("\n");
        return this._getLinkTransferable(data[0], data[1]);

      case FLAVOR_UNICODE:
        return this._getLinkTransferable(aDropData, "");

      case FLAVOR_HTML:
        var result = CC["@mozilla.org/widget/transferable;1"]
                     .createInstance(CI.nsITransferable);
        return this._addTextFlavour(result, FLAVOR_HTML, aDropData);
    }
  }

  // Deal with a selection
  var selection = node.ownerDocument.defaultView.getSelection();
  if (selection.toString().length > 0) {
    return this._getSelectionTransferable(topUrl, topTitle, selection);
  }

  // If we don't find a SRC or HREF tag in this node, then it is most likely an
  // inline element, so we will traverse through its ancestors until we find
  // something we can use.
  var ownerDoc = node.ownerDocument;
  while (node && node != ownerDoc) {
    if (node.src) {
      // If it is an img, make sure the src attribute is the full path
      node.setAttribute("src", node.src);
      var imageURL = node.src;
      var imageTitle = node.src;
      if (node.getAttribute("title")) {
        imageTitle = node.getAttribute("title");
      } else if (node.getAttribute("alt")) {
        imageTitle = node.getAttribute("alt");
      }
      return this._getImageTransferable(imageURL, imageTitle, topUrl);
    }

    if (node.href) {
      // If it is a link make sure the href attribute is the full path
      node.setAttribute("href", node.href);
      var title = node.textContent ? node.textContent : node.title;
      return this._getLinkTransferable(node.href, title);
    }

    // Look at parent
    node = node.parentNode;
  }

  // Don't know... we can return a link to the page
  return this._getLinkTransferable(topUrl, topTitle);
};

// flockIMessage getMessageFromTransferable(in nsITransferable aSubject,
//                                          in unsigned long aListSize,
//                                          [array, size_is(aListSize)]
//                                            in string aFlavorList);
flockRichDragDropService.prototype.getMessageFromTransferable =
function fRDDS_getMessageFromTransferable(aSubject, aListSize, aFlavorList)
{
  var message = {subject: "", body: ""};

  // Get transferable's available flavors
  var transFlavors = aSubject.flavorsTransferableCanExport();
  // Build string array of flavors for easy compare
  var transFlavorArray = [];
  for (var i = 0; i < transFlavors.Count(); i++) {
    transFlavorArray[i] = transFlavors.GetElementAt(i)
                                      .QueryInterface(CI.nsISupportsCString)
                                      .data;
  }

  // Go through list of desired flavors
  for (var i = 0; i < aListSize; i++) {
    // Is this flavor available in the transferable?
    var flavor = aFlavorList[i];
    if (transFlavorArray.indexOf(flavor) != -1) {
      // It is -- try to build a message using it
      message = this._getMessageFromTransferableWithFlavor(aSubject, flavor);
      if (message.body) {
        // Successfully built a message from this transferable
        return message;
      }
    }
  }

  // None of the desired flavors are available in the transferable. If it has a
  // flavor we support, use it instead.
  for (var i = 0; i < transFlavorArray.length; i++) {
    // Get a flavor from the transferable
    var flavor = transFlavorArray[i];
    // Try to build a message using it 
    message = this._getMessageFromTransferableWithFlavor(aSubject, flavor);
    if (message.body) {
      // Successfully built a message from this transferable
      return message;
    }
  }

  // No valid flavor available
  this._logger.debug("Could not find a supported flavor.");

  // We should do something to support it
  throw "No Supported Flavour for Sharing";
};

// AString getBreadcrumb(in AString aType);
flockRichDragDropService.prototype.getBreadcrumb =
function fRDDS_getBreadcrumb(aType) {
  var breadcrumb = ""; 

  // Is breadcrumb enabled?
  var prefService = CC[FLOCK_PREF_SERVICE_CONTRACT_ID]
                    .getService(CI.nsIPrefBranch);

  if (prefService.getPrefType("flock.sharing.breadcrumb.enabled")
      && prefService.getBoolPref("flock.sharing.breadcrumb.enabled"))
  {
    var footer = flockGetString("common/sharing", "flock.sharing.breadcrumb.footer1");
    var url = flockGetString("common/sharing", "flock.sharing.breadcrumb.url");

    // Build breadrcumb based on desired type of formatting
    if (aType == "rich") {
      breadcrumb = "<br /><br />" + footer + "<br />"
                 + "<a href='" +url + "'>" + url + "</a>" + "<br />";
    } else {
      // aType == "plain"
      breadcrumb = "\n\n" + footer + "\n" + url + "\n";
    }
  }

  return breadcrumb;
};

// boolean hasBreadcrumb(in nsIDOMHTMLElement aElement);
flockRichDragDropService.prototype.hasBreadcrumb =
function fRDDS_hasBreadcrumb(aElement) {
  // Get the text of the element, stripped of its mark-up
  var elementText = aElement.value ? aElement.value : aElement.textContent;
  if (!elementText) {
    return false;
  }
  // Look for rich breadcrumb text in element.
  var breadcrumbElement = aElement.ownerDocument.createElement("span");
  breadcrumbElement.innerHTML = this.getBreadcrumb("rich");
  if (elementText.indexOf(breadcrumbElement.textContent) > -1) {
    return true;
  }
  // Look for plain breadcrumb text in the element. Check for case where
  // newlines have been replaced by breaks in rich text editors.
  var plainBreadcrumb = this.getBreadcrumb("plain");
  var modifiedPlainBreadcrumb = plainBreadcrumb.replace(/\n/g, "");
  if (elementText.indexOf(plainBreadcrumb) > -1 ||
      elementText.indexOf(modifiedPlainBreadcrumb) > -1)
  {
    return true;
  }
  // Didn't find it
  return false;
};

//  boolean handleDrop(in nsIDragSession aSession,
//                     in nsIDOMHTMLElement aTargetElement);
flockRichDragDropService.prototype.handleDrop =
function fRDDS_handleDrop(aSession, aTargetElement) {
  if (!(aSession && aTargetElement)) {
    return false;
  }

  // Enumerate all of the services which support rich content dropping
  var catMgr = CC[FLOCK_CATEGORY_MANAGER_CONTRACT_ID]
               .getService(CI.nsICategoryManager);
  var svcEnum = catMgr.enumerateCategory(FLOCK_RICH_CONTENT_CATEGORY);
  if (!svcEnum) {
    return false;
  }

  // Get the transferable with the various rich content flavours
  var transferable = this.getRichSelection(aSession, null, null);
  if (!transferable) {
    return false;
  }

  // Check if the target element belongs to one of the supported services, and
  // if that service can handle the drop
  while (svcEnum.hasMoreElements()) {
    var entry = svcEnum.getNext().QueryInterface(CI.nsISupportsCString);
    if (entry) {
      var contractID = catMgr.getCategoryEntry(FLOCK_RICH_CONTENT_CATEGORY,
                                               entry.data);
      var service = CC[contractID].getService(CI.flockIRichContentDropHandler);
      if (service.handleDrop(transferable, aTargetElement)) {
        // Service handled the rich content drop
        return true;
      }
    }
  }

  // Could not find the appropriate supported service to drop the rich
  // context on, so handle generically here
  if (aTargetElement instanceof CI.nsIDOMHTMLTextAreaElement) {
    // For a text area, drop uber-flav
    var uberFlavour = this._getUberFlavour(transferable);
    if (uberFlavour) {
      this._addToTextarea(uberFlavour, aTargetElement);
      return true;
    }
  } else if (this.isInRichTextEditor(aTargetElement)) {
    // For rich text editors, try to drop HTML
    var content = this._getFlavourData(transferable, FLAVOR_HTML);
    if (!content || this._isRichVideoContent(content)) {
      // If we couldn't get the rich content or it's a video, just get plain
      // text. "Rich textareas" used in mail clients cannot accept HTML OBJECT
      // or EMBED tags
      content = this._getFlavourData(transferable, FLAVOR_UNICODE)
                    .replace(/: /g, ":<br />");
    }
    // Add it to the editor
    if (content) {
      this.addToRichTextEditor(content, aTargetElement);
      return true;
    }
  }

  // Not handled
  return false;
};

// boolean isInRichTextEditor(in nsIDOMHTMLElement aElement);
flockRichDragDropService.prototype.isInRichTextEditor =
function fRDDS_isInRichTextEditor(aElement) {
  return aElement.ownerDocument.designMode == "on";
};

// Check if the selection contains rich video content
flockRichDragDropService.prototype.isSelectionRichVideoContent =
function fRDDS_isSelectionRichVideoContent(aSession) {
  // Get the various rich content flavours
  var transferable = this.getRichSelection(aSession, null, null);
  if (!transferable) {
    return false;
  }

  // Get rich content
  var content = this._getFlavourData(transferable, FLAVOR_HTML);
  return this._isRichVideoContent(content);
};


/**************************************************************************
 * Flock Rich Drag Drop Service Private Members
 **************************************************************************/


flockRichDragDropService.prototype._getMessageFromTransferableWithFlavor =
function fRDDS_getMessageFromTransferableWithFlavor(aSubject, aFlavor) {
  var message = {subject: "", body: ""};
  message.QueryInterface = function (aIID) {
    if (aIID.equals(CI.flockIMessage) || aIID.equals(CI.nsISupports)) {
      return this;
    }
    throw CR.NS_ERROR_NO_INTERFACE;
  };

  // Get content from transferable
  var content = this._getFlavourData(aSubject, aFlavor);
  if (!content) {
    return message;
  }

  // Build message from content
  var contentParts;
  switch (aFlavor) {
    case FLAVOR_MOZ_RDF:
      var coop = CC["@flock.com/singleton;1"]
                 .getService(CI.flockISingleton)
                 .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                 .wrappedJSObject;
      var item = coop.get(content);
      message.subject = item.name;
      if (item.isInstanceOf(coop.FeedItem)) {
        message.body = this._getFlavourData(aSubject, FLAVOR_HTML);
      } else {
        message.body = item.description;
      }
      break;

    case FLAVOR_FLOCK_MEDIA:
    case FLAVOR_MOZ_URL:
      contentParts = content.split("\n");
      message.subject = contentParts[1];
      message.body = contentParts[0];
      break;

    case FLAVOR_TEXT_CITE:
      contentParts = content.split("\n");
      message.subject = contentParts[2];
      message.body = content;
      break;

    case FLAVOR_UNICODE:
      contentParts = content.split(": ");
      if (contentParts.length > 1) {
        message.body = contentParts.pop();
        message.subject = contentParts.join(": ");
      } else {
        message.subject = "";
        message.body = content;
      }
      break;

    case FLAVOR_HTML_CITE:
    case FLAVOR_HTML:
      // Parse the content for the subject
      message.subject = "";
      const RE1 = /^(.+?):<br>/;
      var matches = content.match(RE1) || [];

      for (var idx = 1; idx < matches.length; idx++) {
        if (matches[idx]) {
          message.subject = matches[idx];
          message.body = content.replace(RE1, "");
          return message;
        }
      }

      const RE2 = /^(.+?): <object|<a.+?title="(.+?)"|<img.+?alt="(.+?)"|<a.+?>(.+?)<\/a/;
      matches = content.match(RE2) || [];
      // Iterate through the results to find the first value. If we don't find
      // anything, then the subject wlll be blank
      for (var idx = 1; idx < matches.length; idx++) {
        if (matches[idx]) {
          message.subject = matches[idx];
          break;
        }
      }

      message.body = content;
      break;
  }

  if (!message.body) {
    this._logger.debug("The flavor '"
                       + aFlavor
                       + "' is unsupported by this method");
  }

  return message;
};

// Used code from viewPartialSource.js
flockRichDragDropService.prototype._selectionToHtml =
function fRDDS__selectionToHtml(aSelection) {
  var inst = this;

  /////////////////////////////////////////////////////////////////////////
  // helper to get a path like FIXptr, but with an array
  // instead of the "tumbler" notation
  // see FIXptr:
  // http://lists.w3.org/Archives/Public/www-xml-linking-comments/2001AprJun/att-0074/01-NOTE-FIXptr-20010425.htm
  function getPath(ancestor, node) {
    var n = node;
    var p = n.parentNode;
    if (n == ancestor || !p) {
      return null;
    }
    var path = [];
    if (!path) {
      return null;
    }
    do {
      for (var i = 0; i < p.childNodes.length; i++) {
        if (p.childNodes.item(i) == n) {
          path.push(i);
          break;
        }
      }
      n = p;
      p = n.parentNode;
    } while (n != ancestor && p);
    return path;
  };

  function makeRelativeLinksAbsolute(aBaseUrl, aDOMNode) {
    // This is a recursive method: bail if no tag name
    if (!aDOMNode.tagName) {
      return;
    }

    switch (aDOMNode.tagName.toLowerCase()) {
      case "img":
        var url = aDOMNode.getAttribute("src");
        if (!url.match("http")) {
          aDOMNode.setAttribute("src", inst._makeURLAbsolute(aBaseUrl, url));
        }
        break;

      case "a":
        var url = aDOMNode.getAttribute("href");
        if (url && !url.match("http")) {
          aDOMNode.setAttribute("href", inst._makeURLAbsolute(aBaseUrl, url));
        }
        break;

      default:
        break
    }
    if (aDOMNode.hasChildNodes()) {
      var kids = aDOMNode.childNodes;
      for (var i = 0; i < kids.length; i++) {
        makeRelativeLinksAbsolute(aBaseUrl, kids[i]);
      }
    }
  };

  var range = aSelection.getRangeAt(0);
  var ancestorContainer = range.commonAncestorContainer;
  var doc = ancestorContainer.ownerDocument;

  var startContainer = range.startContainer;
  var endContainer = range.endContainer;
  var startOffset = range.startOffset;
  var endOffset = range.endOffset;

  // let the ancestor be an element
  if (ancestorContainer.nodeType == CI.nsIDOMNode.TEXT_NODE ||
      ancestorContainer.nodeType == CI.nsIDOMNode.CDATA_SECTION_NODE)
  {
    ancestorContainer = ancestorContainer.parentNode;
  }

  // for selectAll, let's use the entire document, including <html>...</html>
  // @see DocumentViewerImpl::SelectAll() for how selectAll is implemented
  try {
    if (ancestorContainer == doc.body) {
      ancestorContainer = doc.documentElement;
    }
  } catch (ex) {
  }

  // each path is a "child sequence" (a.k.a. "tumbler") that
  // descends from the ancestor down to the boundary point
  var startPath = getPath(ancestorContainer, startContainer);
  var endPath = getPath(ancestorContainer, endContainer);

  // clone the fragment of interest and reset everything to be relative to it
  // note: it is with the clone that we operate from now on
  ancestorContainer = ancestorContainer.cloneNode(true);
  startContainer = ancestorContainer;
  endContainer = ancestorContainer;
  var i;
  for (i = startPath ? startPath.length-1 : -1; i >= 0; i--) {
    startContainer = startContainer.childNodes.item(startPath[i]);
  }
  for (i = endPath ? endPath.length-1 : -1; i >= 0; i--) {
    endContainer = endContainer.childNodes.item(endPath[i]);
  }

  // add special markers to record the extent of the selection
  // note: |startOffset| and |endOffset| are interpreted either as
  // offsets in the text data or as child indices (see the Range spec)
  // (here, munging the end point first to keep the start point safe...)
  var tmpNode;
  if (endContainer.nodeType == CI.nsIDOMNode.TEXT_NODE ||
      endContainer.nodeType == CI.nsIDOMNode.CDATA_SECTION_NODE)
  {
    // do some extra tweaks to try to avoid the view-source output to look like
    // ...<tag>]... or ...]</tag>... (where ']' marks the end of selection).
    // To get a neat output, the idea here is to remap the end point from:
    // 1. ...<tag>]...   to   ...]<tag>...
    // 2. ...]</tag>...  to   ...</tag>]...
    if ((endOffset > 0 && endOffset < endContainer.data.length) ||
        !endContainer.parentNode ||
        !endContainer.parentNode.parentNode)
    {
      endContainer.insertData(endOffset, MARK_SELECTION_END);
    } else {
      tmpNode = doc.createTextNode(MARK_SELECTION_END);
      endContainer = endContainer.parentNode;
      if (endOffset == 0) {
        endContainer.parentNode.insertBefore(tmpNode, endContainer);
      } else {
        endContainer.parentNode.insertBefore(tmpNode, endContainer.nextSibling);
      }
    }
  } else {
    tmpNode = doc.createTextNode(MARK_SELECTION_END);
    endContainer.insertBefore(tmpNode, endContainer.childNodes.item(endOffset));
  }

  if (startContainer.nodeType == CI.nsIDOMNode.TEXT_NODE ||
      startContainer.nodeType == CI.nsIDOMNode.CDATA_SECTION_NODE)
  {
    // do some extra tweaks to try to avoid the view-source output to look like
    // ...<tag>[... or ...[</tag>... (where '[' marks the start of selection).
    // To get a neat output, the idea here is to remap the start point from:
    // 1. ...<tag>[...   to   ...[<tag>...
    // 2. ...[</tag>...  to   ...</tag>[...
    if ((startOffset > 0 && startOffset < startContainer.data.length) ||
        !startContainer.parentNode ||
        !startContainer.parentNode.parentNode ||
        startContainer != startContainer.parentNode.lastChild)
    {
      startContainer.insertData(startOffset, MARK_SELECTION_START);
    } else {
      tmpNode = doc.createTextNode(MARK_SELECTION_START);
      startContainer = startContainer.parentNode;
      if (startOffset == 0) {
        startContainer.parentNode.insertBefore(tmpNode, startContainer);
      } else {
        startContainer.parentNode.insertBefore(tmpNode,
                                               startContainer.nextSibling);
      }
    }
  } else {
    tmpNode = doc.createTextNode(MARK_SELECTION_START);
    startContainer.insertBefore(tmpNode,
                                startContainer.childNodes.item(startOffset));
  }

  // now extract, make url absolute and return the HTML source
  tmpNode = doc.createElementNS(NS_XHTML, "div");
  tmpNode.appendChild(ancestorContainer);
  makeRelativeLinksAbsolute(doc.location, tmpNode);

  return tmpNode.innerHTML;
};

flockRichDragDropService.prototype._makeURLAbsolute =
function fRDDS__makeURLAbsolute(aBase, aUrl) {
  try {
    var ioService = CC["@mozilla.org/network/io-service;1"]
                    .getService(CI.nsIIOService);
    var baseURI = ioService.newURI(aBase, null, null);

    return ioService.newURI(baseURI.resolve(aUrl), null, null).spec;
  } catch (ex) {
    // Special URLs throw exceptions, in this case we just return it "as is"
    return aUrl;
  }
};

flockRichDragDropService.prototype._makeHTMLCitation =
function fRDDS__makeHTMLCitation(aUrl, aTitle, aContent) {
  var source = <p class="citation">
                 <cite>
                   <a href={aUrl}>{aTitle}</a>
                 </cite>
               </p>;

  // We don't use E4X here because it would escape "aContent"
  var result = "<blockquote cite=" + aUrl + ">"
             + "<p>" + aContent + "</p>"
             + "</blockquote>"
             + source.toXMLString();

  return result;
};

flockRichDragDropService.prototype._makeTextCitation =
function fRDDS__makeTextCitation(aUrl, aTitle, aContent) {
  var result = aContent + "\n\n"
             + aTitle + "\n"
             + aUrl;

  return result;
};

flockRichDragDropService.prototype._addTextFlavour =
function fRDDS__addTextFlavour(aTransferable,
                               aFlavour,
                               aTextData)
{
  var supports = CC["@mozilla.org/supports-string;1"]
                 .createInstance(CI.nsISupportsString);
  supports.data = aTextData;
  aTransferable.addDataFlavor(aFlavour);
  var length = supports.data.length * 2;
  aTransferable.setTransferData(aFlavour, supports, length);

  return aTransferable;
};

flockRichDragDropService.prototype._getImageTransferable =
function fRDDS__getImageTransferable(aImageUrl,
                                     aImageTitle,
                                     aWebPageUrl)
{
  if (!this._isSharableUrl(aImageUrl)) {
    return null;
  }

  var result = CC["@mozilla.org/widget/transferable;1"]
               .createInstance(CI.nsITransferable);
  
  // text/x-moz-url
  var urltext = aImageUrl + "\n" + aImageTitle;
  this._addTextFlavour(result, FLAVOR_MOZ_URL, urltext);

  // text/x-flock-media
  this._addTextFlavour(result, FLAVOR_FLOCK_MEDIA, urltext);

  // text/unicode
  var text = ((aImageTitle.length) ? (aImageTitle + ": ") : "")
           + aImageUrl;
  this._addTextFlavour(result, FLAVOR_UNICODE, text);

  if (this._isSharableUrl(aWebPageUrl)) {
    // text/html
    var html = <a href={aWebPageUrl} title={aImageTitle}>
                 <img alt={aImageTitle} src={aImageUrl}/>
               </a>;
    this._addTextFlavour(result, FLAVOR_HTML, html.toXMLString());

    // text/x-flock-htmlcitation
    var citation = this._makeHTMLCitation(aWebPageUrl, aImageTitle, html);
    this._addTextFlavour(result, FLAVOR_HTML_CITE, citation);
  } else {
    // text/html
    var html = <a href={aImageUrl} title={aImageTitle}>
                 <img alt={aImageTitle} src={aImageUrl}/>
               </a>;
    this._addTextFlavour(result, FLAVOR_HTML, html.toXMLString());
  }

  return result;
};

flockRichDragDropService.prototype._getLinkTransferable =
function fRDDS__getLinkTransferable(aURL, aTitle) {
  if (!this._isSharableUrl(aURL)) {
    return null;
  }

  var result = CC["@mozilla.org/widget/transferable;1"]
               .createInstance(CI.nsITransferable);
  // text/x-moz-url
  var urltext = aURL + "\n" + aTitle;
  this._addTextFlavour(result, FLAVOR_MOZ_URL, urltext);

  // text/unicode
  var text = aTitle + ": " + aURL;
  this._addTextFlavour(result, FLAVOR_UNICODE, text);

  // text/html
  var html = <a href={aURL}>{aTitle}</a>;
  this._addTextFlavour(result, FLAVOR_HTML, html.toXMLString());

  return result;
};

flockRichDragDropService.prototype._getSelectionTransferable =
function fRDDS__getSelectionTransferable(aURL, aTitle, aSelection) {
  var result = CC["@mozilla.org/widget/transferable;1"]
               .createInstance(CI.nsITransferable);

  var html = this._selectionToHtml(aSelection);
  var text = aTitle + ": " + aSelection.toString();

  if (this._isSharableUrl(aURL)) {
    // text/x-flock-htmlcitation
    var htmlCitation = this._makeHTMLCitation(aURL, aTitle, html)
    this._addTextFlavour(result, FLAVOR_HTML_CITE, htmlCitation);

    // text/x-flock-textcitation
    var textCitation = this._makeTextCitation(aURL, aTitle, text)
    this._addTextFlavour(result, FLAVOR_TEXT_CITE, textCitation);
  }

  // text/unicode
  this._addTextFlavour(result, FLAVOR_UNICODE, text);

  // text/html
  this._addTextFlavour(result, FLAVOR_HTML, aTitle + ":<br>" + html);

  return result;
};

flockRichDragDropService.prototype._getMediabarTransferable =
function fRDDS__getMediabarTransferable(aMedia)
{
  var result = CC["@mozilla.org/widget/transferable;1"]
               .createInstance(CI.nsITransferable);

  var title = aMedia.title.replace(/%20/g, " "); 
  var url = aMedia.webPageUrl;
  var imageUrl = aMedia.midSizePhoto;

  // text/x-moz-url
  var urltext = url + "\n" + title;
  this._addTextFlavour(result, FLAVOR_MOZ_URL, urltext);

  // text/x-flock-media
  var text = url + "\n" + title;
  this._addTextFlavour(result, FLAVOR_FLOCK_MEDIA, text);

  // text/unicode
  var text = ((title.length) ? (title + ": ") : "") + url;
  this._addTextFlavour(result, FLAVOR_UNICODE, text);

  // text/html
  var html;
  if (eval(aMedia.is_video)) {
    // video
    html = aMedia.buildHTML();
    this._addTextFlavour(result, FLAVOR_HTML, html);
  } else {
    // photo
    html = <a href={url} title={title}>
             <img alt={title} src={imageUrl}/>
           </a>;
    this._addTextFlavour(result, FLAVOR_HTML, html.toXMLString());
  }

  // text/x-flock-htmlcitation
  var citation = this._makeHTMLCitation(url, title, html);
  this._addTextFlavour(result, FLAVOR_HTML_CITE, citation);

  return result;
};

flockRichDragDropService.prototype._getWebClipboardTransferable =
function fRDDS__getWebClipboardTransferable(aDropFlavour, aDropData)
{
  if (!aDropFlavour || !aDropData) {
    return null;
  }

  var result = CC["@mozilla.org/widget/transferable;1"]
               .createInstance(CI.nsITransferable);

  switch (aDropFlavour) {
    case FLAVOR_MOZ_URL:
    case FLAVOR_UNICODE:
      // The data is formatted title:\nurl[\n\ntitle:\nurl...] so we need to
      // rearrange it into the Rich DND unicode format
      var data = aDropData;
      var parts = data.split(":\n");
      if (parts.length > 1) {
        data = parts.shift() + ": " + parts.join(":\n");
      }
      this._addTextFlavour(result, FLAVOR_UNICODE, data);
      break;

    case FLAVOR_HTML:
      // Parse the source for the URL of the item's page. If it is a blockquote
      // then we send the text of the quote instead of the URL.
      const RE1 = /<blockquote.+?>([\s\S]*?)<\/blockquote|<a.+?href="(.+?)"|<embed.+?\>(.+?)<\/|<embed.+?src="(.+?)"/;
      var matches = aDropData.match(RE1) || [];

      // Iterate through the results to find the first value. If we don't find
      // it we'll use the HTML src instead.
      var content = aDropData;
      for (var idx = 1; idx < matches.length; idx++) {
        if (matches[idx]) {
          // Grab the matched content
          content = matches[idx];
          // Check for blockquote
          if (matches[0].indexOf("<blockquote") > -1) {
            // Get rid of the mark-up
            content = CC["@mozilla.org/feed-unescapehtml;1"]
                      .getService(CI.nsIScriptableUnescapeHTML)
                      .unescape(content);
          }
          break;
        }
      }

      // Parse the source for the title
      const RE2 = /^(.+?):<br>|^(.+?): <object|<a.+?title="(.+?)"|<img.+?alt="(.+?)"|<a.+?>(.+?)<\/a/;
      matches = aDropData.match(RE2) || [];

      // Iterate through the results to find the first value. If we don't find
      // anything, then the title wlll be blank
      var title = "";
      for (idx = 1; idx < matches.length; idx++) {
        if (matches[idx]) {
          title = matches[idx];
          break;
        }
      }

      // text/unicode
      var textUnicode = ((title.length) ? (title + ": ") : "") + content;
      this._addTextFlavour(result, FLAVOR_UNICODE, textUnicode);

      // text/html
      this._addTextFlavour(result, FLAVOR_HTML, aDropData);
      break;
  }

  return result;
};

// Retrieves the flavour data from the transferable
flockRichDragDropService.prototype._getFlavourData =
function fRDDS__getFlavourData(aTransferable, aFlavor) {
  if (!aTransferable || !aFlavor) {
    return "";
  }

  var dataObj = {};
  var len = {};

  // If the flavor is not in the transferable, then this call will throw
  try {
    aTransferable.getTransferData(aFlavor, dataObj, len);
  } catch (ex) {
    this._logger.debug("The flavor '"
                       + aFlavor
                       + "' is unsupported by this transferable");
    return "";
  }

  return dataObj.value.QueryInterface(CI.nsISupportsString).data;
};

// Check if the content is rich video content
flockRichDragDropService.prototype._isRichVideoContent =
function fRDDS__isRichVideoContent(aContent) {
  // Rich video content will be made up of:
  // 1) an EMBED tag or
  // 2) an EMBED tag within an OBJECT tag
  return aContent && (aContent.toLowerCase().indexOf("<embed ") > -1);
};

// Is this a sharable URL?
flockRichDragDropService.prototype._isSharableUrl =
function fRDDS__isSharableUrl(aUrlString) {
  return (aUrlString &&
          (aUrlString.match(/^(http|https|ftp):\/\/.+/i) ||
           aUrlString.match(/^(mailto):.+/i)));
};

// Get the uber flavour with the desired richness
flockRichDragDropService.prototype._getUberFlavour =
function fRDDS__getUberFlavour(aTransferable, aType) {
  if (!aTransferable) {
    return "";
  }

  var uberFlavour = "";

  var newline = "\n";
  if (aType == "rich") {
    newline = "<br />";
  }

  var link = this._getFlavourData(aTransferable, FLAVOR_UNICODE);
  if (link) {
    uberFlavour = flockGetString("common/sharing",
                                 "flock.sharing.uberFlavour.link")
                + newline + link;
  }

  var content = this._getFlavourData(aTransferable, FLAVOR_HTML);
  if (content) {
    if (uberFlavour) {
      uberFlavour += newline + newline;
    }

    uberFlavour += flockGetString("common/sharing",
                                 "flock.sharing.uberFlavour.content")
                 + newline + this._sanitizeHTMLContent(content);
  }

  return uberFlavour;
};

flockRichDragDropService.prototype._sanitizeHTMLContent =
function fRDDS__sanitizeHTMLContent(aContent) {
  return aContent.replace(/&amp;/g, "&");
};

// Add the content to the text area object
flockRichDragDropService.prototype._addToTextarea =
function fRDDS__addToTextArea(aContent, aTextarea) {
  if (aContent && aTextarea) {
    var caretPos = aTextarea.selectionEnd;
    var currentValue = aTextarea.value;
    var breadcrumb = (aTextarea.value.length == caretPos)
                   ? this.getBreadcrumb("plain")
                   : "";
    aTextarea.value = currentValue.substring(0, caretPos)
                    + aContent
                    + currentValue.substring(caretPos)
                    + breadcrumb;
  }
};

// Determine if the document has a breadcrumb
flockRichDragDropService.prototype._documentHasBreadcrumb =
function fRDDS__documentHasBreadcrumb(aDocument) {
  // Look for our rich DND breadcrumb
  if (aDocument.getElementById(FLOCK_BREADCRUMB_ID)) {
    return true;
  }
  // Try to find the breadcrumb in the body text
  if (this.hasBreadcrumb(aDocument.body)) {
    return true;
  }
  // Didn't find it
  return false;
};


/**************************************************************************
 * END Flock Rich Drag Drop Service Private Members
 **************************************************************************/

/**************************************************************************
 * END Flock Rich Drag Drop Service
 **************************************************************************/

/**************************************************************************
 * XPCOM Support - Module Construction
 **************************************************************************/

// Create array of components.
var componentArray = [flockRichDragDropService];

// Wrap the components in a module.
var module = new FlockXPCOMUtils.genericModule(MODULE_NAME, componentArray);

// Provide XPCOM with access to the module.
function NSGetModule(aCompMgr, aFileSpec) {
  return module;
}

/**************************************************************************
 * END XPCOM Support
 **************************************************************************/
