// 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 CU = Components.utils;
const CR = Components.results;

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

const FM_CONTRACTID = "@flock.com/favorites-migration-service;1";
const FM_CLASSID    = Components.ID("{2e8f8f03-1b5a-42b2-a09f-ba14305fe4fb}");
const FM_CLASSNAME  = "Flock Favorites Migration Service";

const MODULE_NAME = "Favorites Migration";

const OLD_FAVORITES_RDF_FILE = "flock_favorites.rdf";
const OLD_FAVORITES_RDF_FILE_RELIC = "flock_favorites_old.rdf";

const FLOCK_NS = "http://flock.com/rdf#";
const WEB_NS = "http://home.netscape.com/WEB-rdf#";

const DESCRIPTION_ANNO = "bookmarkProperties/description";

function loadSubScript(spec) {
  var loader = CC["@mozilla.org/moz/jssubscript-loader;1"]
               .getService(CI.mozIJSSubScriptLoader);
  var context = {};
  loader.loadSubScript(spec, context);
  return context;
}

/*************************************************************************
 * Component: flockFavesMigrationService
 *************************************************************************/

function flockFavesMigrationService() {
}

/*************************************************************************
 * flockFavesMigrationService: XPCOM Component Creation
 *************************************************************************/

flockFavesMigrationService.prototype = new FlockXPCOMUtils.genericComponent(
  FM_CLASSNAME,
  FM_CLASSID,
  FM_CONTRACTID,
  flockFavesMigrationService,
  CI.nsIClassInfo.SINGLETON,
  [
    CI.flockIMigratable
  ]
);

flockFavesMigrationService.prototype._xpcom_categories = [
  { category: "flockMigratable" }
];

/*************************************************************************
 * flockFavesMigrationService: private members
 *************************************************************************/

flockFavesMigrationService.prototype._init =
function flockFavesMigrationService__init() {
  this._logger = CC["@flock.com/logger;1"].createInstance(CI.flockILogger);
  this._logger.init("favesmigration");

  this._coop = CC["@flock.com/singleton;1"]
               .getService(CI.flockISingleton)
               .getSingleton("chrome://flock/content/common/load-faves-coop.js")
               .wrappedJSObject;

  this._bmsvc = CC["@mozilla.org/browser/nav-bookmarks-service;1"]
                .getService(CI.nsINavBookmarksService);
  this._lmsvc = CC["@mozilla.org/browser/livemark-service;2"]
                .getService(CI.nsILivemarkService);
  this._iconsvc = CC["@mozilla.org/browser/favicon-service;1"]
                  .getService(CI.nsIFaviconService);
  this._tagsvc = CC["@mozilla.org/browser/tagging-service;1"]
                 .getService(CI.nsITaggingService);
  this._annosvc = CC["@mozilla.org/browser/annotation-service;1"]
                  .getService(CI.nsIAnnotationService);
  this._ios = CC["@mozilla.org/network/io-service;1"]
              .getService(CI.nsIIOService);
  this._icondatasvc = CC["@flock.com/favicon-data-service;1"]
                      .getService(CI.flockIFaviconDataService);
}

flockFavesMigrationService.prototype._insertFavorite =
function flockFavesMigrationService__insertFavorite(aParentId,
                                                    aURL,
                                                    aDescription,
                                                    aPosition,
                                                    aName,
                                                    aTags,
                                                    aFavicon) {
  this._logger.debug("Import " + aName + " ; " + aURL);
  try {
    var uri = this._ios.newURI(aURL, null, null);
    var newItemId = this._bmsvc.insertBookmark(aParentId,
                                               uri,
                                               aPosition,
                                               aName);
    if (aDescription) {
      this._annosvc.setItemAnnotation(newItemId,
                                      DESCRIPTION_ANNO,
                                      aDescription,
                                      0,
                                      CI.nsIAnnotationService.EXPIRE_NEVER);
    }
    if (aTags && aTags.length > 0) {
      this._tagsvc.tagURI(uri, aTags.split(","));
    }
    if (aFavicon) {
      this._setFavicon(uri, aFavicon);
    }
    return newItemId;
  } catch(ex) {
    this._logger.error("Import of " + aURL + " failed: " + ex);
    return null;
  }
}

flockFavesMigrationService.prototype._migrateFromCardinal =
function flockFavesMigrationService__migrateFromCardinal(ctxt) {
  const RDFS = CC["@mozilla.org/rdf/rdf-service;1"]
               .getService(CI.nsIRDFService);

  // Namespaces Helpers
  function Namespace(ns) {
    return function Namespace_lambda(arg) {
      return RDFS.GetResource(ns + arg);
    }
  }
  const W3RDF = Namespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#");
  const FLRDF = Namespace(FLOCK_NS);
  const RDF_TYPE = W3RDF("type");
  const RDF_COOPTYPE = FLRDF("CoopType");

  // read into in-memory rdf
  var ds = RDFS.GetDataSourceBlocking(this._ios.newFileURI(ctxt.oldFavoritesFile).spec);

  // coopify the rdf graph
  var resourceCount = 0;
  var resources = ds.GetAllResources();
  while (resources.hasMoreElements()) {
    var resource = resources.getNext();
    var type_target = ds.GetTarget(resource, RDF_TYPE, true);
    if (type_target) {
      ds.Assert(resource, RDF_COOPTYPE, type_target, true);
      resourceCount++;
    }
  }
  var Coop = loadSubScript("chrome://flock/content/common/coop.js").Coop;
  var migration_coop = new Coop(ds,
                                "chrome://flock/content/common/migration-coop.js");
  var folderRes;
  var favEnum = migration_coop.CardinalFavorite.all();
  var favesCount = 0;

  // Migrate unsorted favorites
  var folderId = this._bmsvc.unfiledBookmarksFolder;
  var inst = this;
  var callback = {
    runBatched: function runBatched() {
      while (favEnum.hasMoreElements()) {
        var fave = favEnum.getNext();
        if (!fave.Name) {
          continue;
        }
        if (fave.URL == "http://flock.com/rdf#FavoritesRoot") {
          continue;
        }
        var favParents = fave.getParents();
        if (favParents) {
          // fave.getParents() possibly has an array of null parents. These are
          // the bookmarks that are not put into a collection or bookmarks toolbar
          // in Cardinal. We put them in "unsorted".
          if (favParents.length == 1 && !favParents[0]) {
            inst._insertFavorite(folderId,
                                 fave.URL,
                                 fave.description,
                                 -1,
                                 fave.Name,
                                 fave.tags,
                                 fave.favicon);
          }
        }
        favesCount++;
        var percentage = Math.round(100 * (favesCount / resourceCount));
        ctxt.listener.onUpdate(percentage, "Migrating favorites");
      }

      // Migrate collections and favorites that belong to at least one
      var collEnum = migration_coop.CardinalCollection.all();
      var folderRes = RDFS.GetResource(inst._coop.bookmarks_root.id());
      while (collEnum.hasMoreElements()) {
        var coll = collEnum.getNext();
        var folderId;
        if (!coll || !coll.Name) {
          continue;
        }
        if (coll.toolbar) {
          folderId = inst._bmsvc.toolbarFolder;
        } else {
          folderId = inst._bmsvc.createFolder(inst._bmsvc.bookmarksMenuFolder,
                                              coll.Name,
                                              -1);
        }
        var collContents = coll.children.enumerate();
        while (collContents.hasMoreElements()) {
          fave = collContents.getNext();
          if (!fave) {
            continue;
          }
          inst._insertFavorite(folderId,
                               fave.URL,
                               fave.description,
                               -1,
                               fave.Name,
                               fave.tags,
                               fave.favicon);
        }
      }
    }
  }
  this._bmsvc.runInBatchMode(callback, null);

  // rename the file and delete the old copy
  RDFS.UnregisterDataSource(ds);
  ds = null;
  migration_coop.datasource = null;
  migration_coop = null;

  ctxt.oldFavoritesFile.moveTo(null, OLD_FAVORITES_RDF_FILE_RELIC);

  // Prevent FF2 => FF3 profile migration from happening
  var prefBranch = CC["@mozilla.org/preferences-service;1"]
                   .getService(CI.nsIPrefBranch);
  prefBranch.setBoolPref("browser.places.importBookmarksHTML", false);

  yield true;
}

flockFavesMigrationService.prototype._migrateToolbar =
function flockFavesMigrationService__migrateToolbar(aCoopFolder) {
  var toolbarEnum = aCoopFolder.children.enumerate();
  var toolbarId = this._bmsvc.toolbarFolder;

  while (toolbarEnum.hasMoreElements()) {
    var item = toolbarEnum.getNext();
    this._migrateItem(toolbarId, item);
  }
}

flockFavesMigrationService.prototype._setFavicon =
function flockFavesMigrationService__setFavicon(aPageURI, aFavicon) {
  /**
   * In earlier Flock we were not storing the URL of the favicon, just the data
   * (as a data: URL). So we use a similar hack as Mozilla is doing
   * in nsPlacesImportExportService.cpp: generate a URI for the favicon
   * and associate the data we have to that URI.
   */

  if (!(typeof(aFavicon) == "string" &&
        aFavicon.substring(0, 5) == "data:")) {
    this._logger.warn("The favicon data for "
                      + aPageURI.spec
                      + " is '"
                      + aFavicon
                      + "' which is invalid, ignoring it");
    return;
  }

  var iconUrl = "http://www.flock.com/2005/made-up-favicon/"
              + encodeURIComponent(aPageURI.spec);
  var iconUri = this._ios.newURI(iconUrl, null, null);
  try {
    this._icondatasvc.setFaviconDataFromDataURL(iconUri, aFavicon, 0);
    this._iconsvc.setFaviconUrlForPage(aPageURI, iconUri);
  } catch (ex) {
    this._logger.error("Failed to set the favicon data for "
                       + aPageURI.spec
                       + ": "
                       + ex);
  }
}

flockFavesMigrationService.prototype._migrateItem =
function flockFavesMigrationService__migrateItem(aPlacesParentId, aCoopItem) {
  var toolbarFolderId = this._coop.toolbar.folder.id();

  if (aCoopItem.id() == toolbarFolderId) {
    this._migrateToolbar(aCoopItem);
  } else if (aCoopItem.isInstanceOf(this._coop.Favorite)) {
    this._insertFavorite(aPlacesParentId,
                         aCoopItem.URL,
                         aCoopItem.description,
                         -1,
                         aCoopItem.name,
                         aCoopItem.tags);
    try {
      var uri = this._ios.newURI(aCoopItem.URL, null, null);
      if (aCoopItem.favicon) {
        this._setFavicon(uri, aCoopItem.favicon);
      }
    } catch(ex) {
      this._logger.error(ex);
    }
  } else if (aCoopItem.isInstanceOf(this._coop.Separator)) {
    this._bmsvc.insertSeparator(aPlacesParentId, -1);
  } else if (aCoopItem.isInstanceOf(this._coop.Folder)) {
    var newFolderId = this._bmsvc.createFolder(aPlacesParentId,
                                               aCoopItem.name,
                                               -1);
    if (aCoopItem.description) {
      this._annosvc.setItemAnnotation(newFolderId,
                                      DESCRIPTION_ANNO,
                                      aCoopItem.description,
                                      0,
                                      CI.nsIAnnotationService.EXPIRE_NEVER);
    }
    var childrenEnum = aCoopItem.children.enumerate();
    while (childrenEnum.hasMoreElements()) {
      var child = childrenEnum.getNext();
      if (!child) {
        continue;
      }
      this._migrateItem(newFolderId, child);
    }
  } else if (aCoopItem.isInstanceOf(this._coop.Feed)) {
    try {
      var uri = this._ios.newURI(aCoopItem.URL, null, null);
      this._lmsvc.createLivemarkFolderOnly(aPlacesParentId,
                                           aCoopItem.name,
                                           null,
                                           uri,
                                           -1);
    } catch(ex) {
      this._logger.error("Import of livemark "
                         + aCoopItem.URL
                         + " failed: "
                         + ex);
    }
  }
}

flockFavesMigrationService.prototype._migrateFromCoop =
function flockFavesMigrationService__migrateFromCoop(ctxt) {
  // Migrate local favorites
  var menuFolder = this._bmsvc.bookmarksMenuFolder;
  this._bmsvc.removeFolderChildren(menuFolder);
  var inst = this;
  var callback = {
    runBatched: function runBatched() {
      var topLevelEnum = inst._coop.bookmarks_root.children.enumerate();
      while (topLevelEnum.hasMoreElements()) {
        var item = topLevelEnum.getNext();
        if (!item) {
          continue;
        }
        inst._migrateItem(menuFolder, item);
      }
    }
  }
  this._bmsvc.runInBatchMode(callback, null);

  // Prevent FF2 => FF3 profile migration from happening
  var prefBranch = CC["@mozilla.org/preferences-service;1"]
                   .getService(CI.nsIPrefBranch);
  prefBranch.setBoolPref("browser.places.importBookmarksHTML", false);

  yield true;
}


/*************************************************************************
 * flockFavesMigrationService: flockIMigratable implementation
 *************************************************************************/

flockFavesMigrationService.prototype.__defineGetter__("migrationName",
function getter_migrationName() {
  return flockGetString("common/migrationNames", "migration.name.favorites");
});

flockFavesMigrationService.prototype.needsMigration =
function needsMigration(aOldVersion) {
  this._init();
  this._logger.debug(".needsMigration('" + aOldVersion + "')");

  var major = parseInt(aOldVersion[0], 10);
  var middle = parseInt(aOldVersion[2], 10); // aOldVersion[1] == "."
  return ((major == 0) || // Cardinal 0.7, Cormorant 0.9
          (major == 1 && middle < 3)); // Falcon 1.0, Grackle 1.1, Grouse 1.2
}

flockFavesMigrationService.prototype.startMigration =
function startMigration(aOldVersion, aFlockMigrationProgressListener) {
  this._logger.debug(".startMigration('"
                     + aOldVersion
                     + "', 'aFlockMigrationProgressListener')");

  /**
   * Favorites are now stored in Places (FF3's bookmark system)
   * There are 2 kind of migrations:
   *  o Version 0.7: direct RDF, flock-favorites.rdf
   *  o Versions 0.9 to 1.2: coop
   * Migration from 0.7 is done first by migrating to coop, getting to the state
   * of a 1.0, then migrating from coop to Places.
   */

  var oldFavoritesFile = null;
  if(aOldVersion.substr(0, 3) == "0.7") {
    oldFavoritesFile = CC["@mozilla.org/file/directory_service;1"]
                       .getService(CI.nsIProperties)
                       .get("ProfD", CI.nsIFile);
    oldFavoritesFile.append(OLD_FAVORITES_RDF_FILE);
  }

  var ctxt = {
    oldVersion: aOldVersion,
    listener: aFlockMigrationProgressListener,
    oldFavoritesFile: oldFavoritesFile
  };

  ctxt.listener.onUpdate(0, "Migrating favorites");
  return {
    wrappedJSObject: ctxt
  };
}

flockFavesMigrationService.prototype.finishMigration =
function finishMigration(ctxtWrapper) {
}

flockFavesMigrationService.prototype.doMigrationWork =
function doMigrationWork(ctxtWrapper) {
  var ctxt = ctxtWrapper.wrappedJSObject;

  if (!ctxt.favoritesMigrator) {
    if (ctxt.oldVersion.substr(0, 3) == "0.7") {
      ctxt.favoritesMigrator = this._migrateFromCardinal(ctxt);
    } else {
      ctxt.favoritesMigrator = this._migrateFromCoop(ctxt);
    }
  }

  if (ctxt.favoritesMigrator.next()) {
    ctxt.favoritesMigrator = null;
  }

  return Boolean(ctxt.favoritesMigrator);
}

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

// Create array of components.
var componentsArray = [flockFavesMigrationService];

// Generate a module for XPCOM to find.
var NSGetModule = FlockXPCOMUtils.generateNSGetModule(MODULE_NAME,
                                                      componentsArray);
