// BEGIN FLOCK GPL
//
// Copyright Flock Inc. 2005-2008
// 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/FlockXPCOMUtils.jsm");
FlockXPCOMUtils.debug = false;

CU.import("resource:///modules/FlockSvcUtils.jsm");
CU.import("resource:///modules/deliciousApi.jsm");
CU.import("resource:///modules/onlineFavoritesBackend.jsm");

const MODULE_NAME = "Magnolia";

const CLASS_NAME = "Flock Magnolia Service";
const CLASS_SHORT_NAME = "magnolia"
const CLASS_TITLE = "Magnolia"
const CLASS_ID = Components.ID("{7c9aa278-85ed-4cbe-b0bc-f35df0978ade}");
const CONTRACT_ID = "@flock.com/magnolia-service;1";

const SERVICE_ENABLED_PREF = "flock.service.magnolia.enabled";

const MAGNOLIA_STRING_BUNDLE = "chrome://flock/locale/services/magnolia.properties";
const MAGNOLIA_URL = "http://ma.gnolia.com";
const MAGNOLIA_FAVICON = "http://ma.gnolia.com/favicon.ico";
const MAGNOLIA_API_URL = "https://ma.gnolia.com/api/mirrord/v1/";
const MAGNOLIA_USER_URL_BASE = "http://ma.gnolia.com/people";

// One minute, expressed in nsITimer terms.
const TIMER_SECOND = 1000;

/**************************************************************************
 * Component: Flock Magnolia Service
 **************************************************************************/

// Constructor.
function flockMagnoliaService() {
  this._init();

  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "getAccount");
  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "getAccounts");
  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "logout");

  FlockSvcUtils.flockIManageableWebService
               .addDefaultMethod(this, "docRepresentsSuccessfulLogin");
  FlockSvcUtils.flockIManageableWebService
               .addDefaultMethod(this, "getAccountIDFromDocument");
  FlockSvcUtils.flockIManageableWebService
               .addDefaultMethod(this, "getCredentialsFromForm");
  FlockSvcUtils.flockIManageableWebService
               .addDefaultMethod(this, "ownsDocument");
  FlockSvcUtils.flockIManageableWebService
               .addDefaultMethod(this, "ownsLoginForm");
}


/**************************************************************************
 * Flock Magnolia Service: XPCOM Component Creation
 **************************************************************************/

flockMagnoliaService.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME,
  CLASS_ID,
  CONTRACT_ID,
  flockMagnoliaService,
  CI.nsIClassInfo.SINGLETON,
  [
    CI.nsIObserver,
    CI.nsITimerCallback,
    CI.flockIWebService,
    CI.flockIManageableWebService,
    CI.flockIBookmarkWebService,
    CI.flockIPollingService
  ]
);

// FlockXPCOMUtils.genericModule() categories
flockMagnoliaService.prototype._xpcom_categories = [
  { category: "wsm-startup" },
  { category: "flockWebService", entry: CLASS_SHORT_NAME }
];


/**************************************************************************
 * Flock Magnolia Service: Private Data and Functions
 **************************************************************************/

// Member variables.

flockMagnoliaService.prototype._init =
function MagnoliaSvc__init() {
  FlockSvcUtils.getLogger(this).info(".init()");

  // Determine whether this service has been disabled, and unregister if so.
  prefService = CC["@mozilla.org/preferences-service;1"]
                .getService(CI.nsIPrefBranch);
  if (prefService.getPrefType(SERVICE_ENABLED_PREF) &&
     !prefService.getBoolPref(SERVICE_ENABLED_PREF))
  {
    this._logger.info("Pref " + SERVICE_ENABLED_PREF
                              + " set to FALSE... not initializing.");
    this.deleteCategories();
    return;
  }

  profiler = CC["@flock.com/profiler;1"].getService(CI.flockIProfiler);
  var evtID = profiler.profileEventStart("magnolia-init");

  var obs = CC["@mozilla.org/observer-service;1"]
            .getService(CI.nsIObserverService);
  obs.addObserver(this, "xpcom-shutdown", false);

  this._api = new DeliciousAPI(MAGNOLIA_API_URL, this._logger);
  this._accountClassCtor = flockMagnoliaAccount;
  FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.getCoop(this);
  if (this._coop.Service.exists(this.urn)) {
    this._c_svc = this._coop.get(this.urn);
  } else {
    this._c_svc = new this._coop.Service(this.urn, {
      name: CLASS_SHORT_NAME,
      desc: CLASS_TITLE
    });
  }
  this._c_svc.serviceId = CONTRACT_ID;
  this._c_svc.domains = FlockSvcUtils.getWD(this)
                        .getString("magnolia", "domains", "gnolia.com");

  profiler.profileEventEnd(evtID, "");
}

// Helper to retrieve tags on server.
flockMagnoliaService.prototype._getAllTags =
function MagnoliaSvc__getAllTags(aUrn, aAccountId,
                                 aPassword, aPollingListener) {
  this._logger.debug("getAllTags(...)");
  var svc = this;

  var flockListener = {
    onStart: function MagnoliaSvc__getAllTags_onStart(aSubject, aTopic) {
      // Not used.
    },
    onSuccess: function MagnoliaSvc__getAllTags_onSuccess(aSubject, aTopic) {
      svc._logger.info("tags/get SUCCEEDED: " + aTopic);

      var tags = [];
      for (var i = 0; i < aSubject.length; i++) {
        tags.push(aSubject[i].tag);
      }
      onlineFavoritesBackend.updateTags(svc, aAccountId, tags);

      // Tell the poller we're done
      if (aPollingListener) {
        aPollingListener.onResult();
      }
    },
    onError: function MagnoliaSvc__getAllTags_onError(aSubject, aTopic, aError) {
      svc._logger.error("tags/get FAILED");
      // BUG: 5705 -  report error via notification bar?
      // onlineFavoritesBackend.showNotification(message);
      if (aPollingListener) {
        aPollingListener.onError(aError);
      }
    }
  }

  this._api.tagsGet(flockListener, null);
}


/**************************************************************************
 * Flock Magnolia Service: flockIWebService Implementation
 **************************************************************************/

// readonly attribute AString urn;
flockMagnoliaService.prototype.urn = "urn:" + CLASS_SHORT_NAME + ":service";

// readonly attribute AString title;
flockMagnoliaService.prototype.title = CLASS_TITLE;

// readonly attribute AString icon;
flockMagnoliaService.prototype.icon = MAGNOLIA_FAVICON;

// readonly attribute AString url;
flockMagnoliaService.prototype.url = MAGNOLIA_URL;

// readonly attribute AString contractId;
// ALMOST duplicated by nsIClassInfo::contractID
// with different case: contractId != contractID
flockMagnoliaService.prototype.contractId = CONTRACT_ID;

// readonly attribute AString shortName;
flockMagnoliaService.prototype.shortName = CLASS_SHORT_NAME;

// readonly attribute boolean needPassword;
flockMagnoliaService.prototype.needPassword = true;

// flockIWebServiceAccount addAccountById(in AString aAccountID,
//                                        in boolean aIsTransient,
//                                        in flockIListener aListener);
flockMagnoliaService.prototype.addAccountById =
function MagnoliaSvc_addAccountById(aAccountID, aIsTransient, aListener) {
  this._logger.debug("addAccountById('" + aAccountID + "', "
                     + aIsTransient + ", aListener)");
  var acct = onlineFavoritesBackend.createAccount(this,
                                                  aAccountID, aIsTransient);
  if (aListener) {
    aListener.onSuccess(acct, "addAccount");
  }
  return acct;
}

// void removeAccount(in AString aUrn);
flockMagnoliaService.prototype.removeAccount =
function MagnoliaSvc_removeAccount(aUrn) {
  this._logger.debug("removeAccount('" + aUrn + "')");
  onlineFavoritesBackend.removeAccount(this, aUrn);
}

// DEFAULT: flockIWebServiceAccount getAccount(in AString aUrn);
// DEFAULT: nsISimpleEnumerator getAccounts();
// DEFAULT: void logout();

// readonly attribute long status;
flockMagnoliaService.prototype.status = CI.flockIWebService.STATUS_UNKNOWN;


/**************************************************************************
 * Flock Magnolia Service: flockIManageableWebService Implementation
 **************************************************************************/

// DEFAULT: boolean docRepresentsSuccessfulLogin(in nsIDOMHTMLDocument aDocument);
// DEFAULT: AString getAccountIDFromDocument(in nsIDOMHTMLDocument aDocument);
// DEFAULT: nsIPassword getCredentialsFromForm(in nsIDOMHTMLFormElement aForm);
// DEFAULT: boolean ownsDocument(in nsIDOMHTMLDocument aDocument);
// DEFAULT: boolean ownsLoginForm(in nsIDOMHTMLFormElement aForm);

// void updateAccountStatusFromDocument(in nsIDOMHTMLDocument aDocument);
flockMagnoliaService.prototype.updateAccountStatusFromDocument =
function MagnoliaSvc_updateAccountStatusFromDocument(aDocument) {
  this._logger.debug("updateAccountStatusFromDocument()");

  if (this._WebDetective.detect("magnolia", "loggedout", aDocument, null)) {
    this._acUtils.markAllAccountsAsLoggedOut(CONTRACT_ID);
  } else {
    if (this._WebDetective.detect("magnolia", "loggedin", aDocument, null)) {
      var results = FlockSvcUtils.newResults();
      if (this._WebDetective.detect("magnolia",
                                    "accountinfo", aDocument, results))
      {
        var accountID = results.getPropertyAsAString("accountid");
        var accountURN = this._acUtils.getAccountURNById(this.urn, accountID);
        this._acUtils.ensureOnlyAuthenticatedAccount(accountURN);
      }
    }
  }
}


/**************************************************************************
 * Flock Magnolia Service: flockIBookmarkWebService Implementation
 **************************************************************************/

// void publish (in flockIListener aListener, in AString aAccountId,
//               in flockIBookmark aBookmark, in boolean aPrivate);
flockMagnoliaService.prototype.publish =
function MagnoliaSvc_publish(aListener, aAccountId, aBookmark, aPrivate) {
  this._logger.info("Publish(" + aBookmark.URL + "," + aBookmark.name
                    + ") to " + aAccountId + "@Magnolia");
  var accountUrn = this.urn + ":" + aAccountId;
  var password = this._acUtils.getPassword(accountUrn);
  var svc = this;
  var args = {
    url: aBookmark.URL,
    description: aBookmark.name,
    extended: aBookmark.description,
    tags: (aBookmark.tags ? aBookmark.tags.split(/[\s,]/).sort().join(",") : "")
  };
  if (aPrivate) {
    args.shared = "no";
  }

  var deliciousApiListener = {
    onError: function MagnoliaSvc_publish_onError(aError) {
      svc._logger.error("posts/add FAILED");
      // BUG: 5705 -  report error via notification bar?
      // onlineFavoritesBackend.showNotification(message);
      if (aListener) {
        aListener.onError(svc, "publish", aError);
      }
    },
    onSuccess: function MagnoliaSvc_publish_onSuccess(aXML) {
      svc._logger.info("posts/add SUCCEEDED");

      // Validate the nsIDOMDocument response.
      if (!svc._api.isExpectedResponse(aXML, "result")) {
        svc._logger.error("posts/add succeeded - but with invalid xml response");
        // BUG: 5705 -  report error via notification bar?
        // onlineFavoritesBackend.showNotification(message);
        if (aListener) {
          var error = CC["@flock.com/error;1"].createInstance(CI.flockIError);
          // FIXME: fill in this flockIError.
          error.errorCode = CI.flockIError.FAVS_UNKNOWN_ERROR;
          aListener.onError(svc, "publish", error);
        }
        return;
      } else {
        // Process the validated response.
        var result = aXML.documentElement.getAttribute("code");
        svc._logger.debug(" Result for posts/add: " + result);
        if (result == "done") {
          var localBookmarks = svc._coop.get(svc.urn + ":"
                                             + aAccountId + ":bookmarks");
          var tags = aBookmark.tags.split(" ");
          for (i in tags) {
            localBookmarks.tag.addOnce(tags[i]);
          }
          onlineFavoritesBackend.updateBookmark(svc, accountUrn,
                                                aBookmark, aPrivate);
          if (aListener) {
            aListener.onSuccess(svc, "publish");
          }
        } else if (aListener) {
          var error = CC["@flock.com/error;1"].createInstance(CI.flockIError);
          // FIXME: fill in this flockIError.
          error.errorCode = CI.flockIError.FAVS_UNKNOWN_ERROR;
          aListener.onError(svc, "publish", error);
        }
      }
    }
  }

  this._api.call("posts/add", args, deliciousApiListener, password);
}

// void publishList (in flockIListener aListener, in AString aAccountId,
//                   in nsISimpleEnumerator aBookmarkList, in boolean aPrivate);
flockMagnoliaService.prototype.publishList =
function MagnoliaSvc_publishList(aListener, aAccountId, aBookmarkList, aPrivate) {
  var delay = 1000; // 1 second between each query
  var svc = this;
  var i = 0;
  var bookmarks = [];

  var syncCallback = {
    notify: function publishListCallback_notify(timer) {
      var bm = bookmarks.shift();
      svc.publish(aListener, aAccountId, bm, aPrivate);
    }
  }

  while (aBookmarkList.hasMoreElements()) {
    var bookmark = aBookmarkList.getNext().QueryInterface(CI.flockIBookmark);
    // Duplicate it because it's going to be removed too early
    bookmarks[i] = {};
    bookmarks[i].URL = bookmark.URL;
    bookmarks[i].name = bookmark.name;
    bookmarks[i].description = bookmark.description;
    bookmarks[i].tags = bookmark.tags;
    bookmarks[i].time = bookmark.time;

    this._logger.debug("Set a timer to " + i * delay
                       + " for " + bookmarks[i].URL);
    var publishTimer = CC["@mozilla.org/timer;1"]
                       .createInstance(CI.nsITimer);
    publishTimer.initWithCallback(syncCallback, i * TIMER_SECOND,
                                  CI.nsITimer.TYPE_ONE_SHOT);
    i++;
  }
}

// void remove (in flockIListener aListener, in AString aAccountId,
//              in AString aBookmark);
flockMagnoliaService.prototype.remove =
function MagnoliaSvc_remove(aListener, aAccountId, aBookmark) {
  this._logger.info("Remove " + aBookmark + " from "
                    + aAccountId + "@Magnolia");
  var password = this._acUtils.getPassword(this.urn + ":" + aAccountId);
  var svc = this;
  var args = { url: aBookmark };

  var deliciousApiListener = {
    onError: function MagnoliaSvc_remove_onError(aError) {
      svc._logger.error("posts/delete FAILED");
      // BUG: 5705 -  report error via notification bar?
      // onlineFavoritesBackend.showNotification(message);
      if (aListener) {
        aListener.onError(svc, "remove", aError);
      }
    },
    onSuccess: function MagnoliaSvc_remove_onSuccess(aXML) {
      svc._logger.info("posts/delete SUCCEEDED");

      // Validate the nsIDOMDocument response.
      if (!svc._api.isExpectedResponse(aXML, "result")) {
        svc._logger.error("posts/delete succeeded - but with invalid xml response");
        // BUG: 5705 -  report error via notification bar?
        // onlineFavoritesBackend.showNotification(message);
        if (aListener) {
          var error = CC["@flock.com/error;1"].createInstance(CI.flockIError);
          // FIXME: fill in this flockIError.
          error.errorCode = CI.flockIError.FAVS_UNKNOWN_ERROR;
          aListener.onError(svc, "remove", error);
        }
      } else {
        // Process the validated response.
        var result = aXML.documentElement.getAttribute("code");
        svc._logger.debug("Result for posts/delete: " + result);
        if (result == "done") {
          onlineFavoritesBackend.destroyBookmark(svc, aAccountId, aBookmark);
          CC["@flock.com/poller-service;1"].getService(CI.flockIPollerService)
          .forceRefresh(this.urn + ":" + aAccountId + ":bookmarks");
          if (aListener) {
            aListener.onSuccess(svc, "remove");
          }
        } else if (aListener) {
          var error = CC["@flock.com/error;1"].createInstance(CI.flockIError);
          // FIXME: fill in this flockIError.
          error.errorCode = CI.flockIError.FAVS_UNKNOWN_ERROR;
          aListener.onError(svc, "remove", error);
        }
      }
    }
  }

  this._api.call("posts/delete", args, deliciousApiListener, password);
}

// boolean exists (in AString aAccountId, in AString aURL);
flockMagnoliaService.prototype.exists =
function MagnoliaSvc_exists(aAccountId, aURL) {
  return this._coop.Bookmark.exists(this.urn + ":" + aAccountId + ":" + aURL);
}

// AString getUserURL (in AString aAccountId);
flockMagnoliaService.prototype.getUserUrl =
function MagnoliaSvc_getUserUrl(aAccountId) {
  return MAGNOLIA_USER_URL_BASE + "/" + aAccountId;
}


/**************************************************************************
 * Flock Magnolia Service: flockIPollingService Implementation
 **************************************************************************/

// void refresh(in AString aUrn, in flockIPollingListener aListener);
flockMagnoliaService.prototype.refresh =
function MagnoliaSvc_refresh(aURN, aPollingListener) {
  var svc = this;
  this._logger.info("refresh(" + aURN + ")");

  if (!this._coop.OnlineBookmarksStream.exists(aURN)) {
    throw "flockMagnoliaService: " + aURN + " can't be found";
  }

  var bookmarks = this._coop.get(aURN);
  var accountId = bookmarks.userid;
  var accountUrn = this.urn + ":" + accountId;
  // nsIPassword for auth for this sync
  var password = this._acUtils.getPassword(accountUrn);

  var flockListener = {
    onStart: function MagnoliaSvc_refresh_onStart(aSubject, aTopic) {
      // Not used.
    },
    onSuccess: function MagnoliaSvc_refresh_onSuccess(aSubject, aTopic) {
      svc._logger.info("posts/all SUCCEEDED: " + aTopic);

      if (aTopic != "nonew") {
        onlineFavoritesBackend.updateLocal(svc, aSubject, aTopic, accountUrn);
        // Now refresh the tag list (wait one second)
        var syncCallback = {
          notify: function refreshCallback_notify(aTimer) {
            svc._getAllTags(aURN, accountId, password, aPollingListener);
          }
        }
        svc._logger.debug("Setting timer to execute _getAllTags()");
        svc._timer = CC["@mozilla.org/timer;1"].createInstance(CI.nsITimer);
        svc._timer.initWithCallback(syncCallback, TIMER_SECOND,
                                    CI.nsITimer.TYPE_ONE_SHOT);
        // For "updated" case, syncCallback calls onResult().
      } else if (aPollingListener) {
        aPollingListener.onResult();
      }
    },
    onError: function MagnoliaSvc_refresh_onError(aSubject, aTopic, aError) {
      svc._logger.error("posts/all FAILED: ["
                        + aError.errorCode + "] "
                        + aError.errorString + " (["
                        + aError.serviceErrorCode + "] "
                        + aError.serviceErrorString + ")");
      var sbs = CC["@mozilla.org/intl/stringbundle;1"]
                .getService(CI.nsIStringBundleService);
      var bundle = sbs.createBundle(MAGNOLIA_STRING_BUNDLE);
      var message;
      switch (aError.errorCode) {
        case CI.flockIError.FAVS_UNAVAILABLE:
          message = bundle.GetStringFromName("flock.magnolia.error.unavailable");
          break;
        case CI.flockIError.FAVS_INVALID_AUTH:
          message = bundle.GetStringFromName("flock.magnolia.error.invalid");
          break;
        default:
          var stringName = "flock.magnolia.error";
          message = bundle.formatStringFromName(stringName,
                                                [aError.serviceErrorString,
                                                 aError.errorServiceCode],
                                                2);
          break;
      }
      onlineFavoritesBackend.showNotification(message);
      if (aPollingListener) {
        aPollingListener.onError(aError);
      }
    }
  }

  // even for the first refresh, we need to get the last update time from 
  // the server. Bruno
  this._api.postsUpdate(flockListener, password, bookmarks.last_update_time);
}

/**************************************************************************
 * Flock Magnolia Service: nsIObserver Implementation
 **************************************************************************/

flockMagnoliaService.prototype.observe =
function MagnoliaSvc_observe(aSubject, aTopic, aState) {
  switch (aTopic) {
    case "xpcom-shutdown":
      var obs = CC["@mozilla.org/observer-service;1"]
                .getService(CI.nsIObserverService);
      obs.removeObserver(this, "xpcom-shutdown");
      break;
  }
}

/**************************************************************************
 * Component: Flock Magnolia Account
 **************************************************************************/

function flockMagnoliaAccount() {
  FlockSvcUtils.getLogger(this).info(".init()");
  FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.getCoop(this);

  FlockSvcUtils.flockIWebServiceAccount.addDefaultMethod(this, "deactivate");
  FlockSvcUtils.flockIWebServiceAccount.addDefaultMethod(this, "login");
  FlockSvcUtils.flockIWebServiceAccount.addDefaultMethod(this, "logout");
  FlockSvcUtils.flockIWebServiceAccount.addDefaultMethod(this, "remove");
}

/**************************************************************************
 * Flock Magnolia Account: XPCOM Component Creation
 **************************************************************************/

// Use genericComponent() for the goodness it provides (QI, nsIClassInfo, etc),
// but do NOT add flockMagnoliaAccount to the list of constructors passed
// to FlockXPCOMUtils.genericModule().
flockMagnoliaAccount.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME + " Account",
  "",
  "",
  flockMagnoliaAccount,
  0,
  [
    CI.flockIWebServiceAccount,
    CI.flockIBookmarkWebServiceAccount,
  ]
  );

/**************************************************************************
 * Flock Magnolia Account: flockIWebServiceAccount Implementation
 **************************************************************************/

// readonly attribute AString urn;
flockMagnoliaAccount.prototype.urn = "";

// void activate(in flockIListener aListener);
flockMagnoliaAccount.prototype.activate =
function MagnoliaAcct_activate(aListener) {
  this._logger.debug("activate()");
  this._coop.get(this.urn).isAuthenticated = true;
  this._coop.get(this.urn + ":bookmarks").isPollable = true;
  if (aListener) {
    aListener.onSuccess(this, "activate");
  }
}

// DEFAULT: void deactivate(in flockIListener aListener);
// DEFAULT: void login(in flockIListener aListener);
// DEFAULT: void logout(in flockIListener aListener);

// void keep();
flockMagnoliaAccount.prototype.keep =
function MagnoliaAcct_keep() {
  this._logger.debug("keep()");
  this._coop.get(this.urn).isTransient = false;
  this._coop.get(this.urn+":bookmarks").isTransient = false;
  this._acUtils.makeTempPasswordPermanent(this.urn);
}

// DEFAULT: void remove();


/**************************************************************************
 * Flock Magnolia Account: flockIBookmarkWebServiceAccount Implementation
 **************************************************************************/

/* No methods or properties on this interface! */


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

// Create array of components.
var gComponentsArray = [flockMagnoliaService];

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

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