// 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

// This is a library to access any bookmark service through the Delicious API.
// Many services are providing a compatibility layer with Delicious,
// so this library is used also for Magnolia and Shadows.

var EXPORTED_SYMBOLS = ["DeliciousAPI"];

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

CU.import("resource://gre/modules/ISO8601DateUtils.jsm");
CU.import("resource:///modules/FlockXMLUtils.jsm");

/**************************************************************************
 * Module: Delicious API
 **************************************************************************/

// Constructor.
function DeliciousAPI(aUrl, aLogger) {
  this._url = aUrl;
  this._logger = aLogger;
}

// Private data.
DeliciousAPI.prototype._url = "";
DeliciousAPI.prototype._logger = null;

/**************************************************************************
 * Delicious API Public Interface
 **************************************************************************/

/**
 * Call the specified del.ico.us API method.
 * @param AString aAPIMethod (in)
 *    One of the API methods; "posts/add", etc.
 * @param object aArgs (in)
 *    As documented at http://del.icio.us/help/api/
 * @param object aAPIListener (in)
 *    An object (deliciousAPIListener) with the following two methods:
 *      void function onSuccess(in nsIDOMDocument aXML)
 *      void function onError(in flockIError aError)
 * @param object aAuth (in)
 *    An object with the following two properties:
 *      AString user
 *      AString password
 * @return nothing
 */
DeliciousAPI.prototype.call =
function deliciousAPI_call(aAPIMethod, aArgs, aAPIListener, aAuth) {
  this._logger.debug("deliciousAPI_call('" + aAPIMethod + "', aArgs,"
                     + " aAPIListener, aAuth)");

  // Convert args from object notation to an array of "key=value" pairs,
  // performing any escaping necessary for use in URL.
  var argsArray = [];
  for (var key in aArgs) {
    argsArray.push(encodeURIComponent(key) + "="
                   + encodeURIComponent(aArgs[key]));
  }

  // Build the complete API command: host, API method, and args.
  var url = this._url + aAPIMethod;
  if (argsArray.length) {
    url = url + "?" +argsArray.join("&");
  }

  // Build an XMLHTTPRequest for sending the API call.
  var request = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
                .createInstance(CI.nsIXMLHttpRequest);

  var api = this;
  var onReadyStateFunc = function deliciousAPI_call_onReadyStateFunc(eEvt) {
    if (request.readyState == 4) {
      var error;
      var status = request.status; // does not throw anymore in FF3 base
      if (status >= 200 && status < 300) {
        // Scrub input and create an nsIDOMDocument.
        var text = request.responseText.replace(/[\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F]/g, "");
        if (!text) {
          text = "<?xml version='1.0' standalone='yes'?>";
        }
        var xml = CC["@mozilla.org/xmlextras/domparser;1"]
                  .createInstance(CI.nsIDOMParser)
                  .parseFromString(text, "text/xml");
        aAPIListener.onSuccess(xml);
        return;
      }
      error = CC["@flock.com/error;1"].createInstance(CI.flockIError);
      error.serviceErrorCode = status;
      error.serviceErrorString = request.responseText;
      switch (status) {
        case 0:
          // Internet Connection lost
          error.errorCode = CI.flockIError.HTTP_CONNECTION_LOST;
          break;
        case 401:
          // Bad login/password
          error.errorCode = CI.flockIError.FAVS_INVALID_AUTH;
          break;
        case 503:
          // Service unavailable
          error.errorCode = CI.flockIError.FAVS_UNAVAILABLE;
          break;
        default:
          // Unknown error code
          error.errorCode = CI.flockIError.FAVS_UNKNOWN_ERROR;
          break;
      }
      aAPIListener.onError(error);
    }
  };

  request.onreadystatechange = onReadyStateFunc;
  request.mozBackgroundRequest = true;
  request.overrideMimeType("text/txt");
  if (aAuth) {
    request.open("GET", url, true, aAuth.username, aAuth.password);
  } else {
    request.open("GET", url, true);
  }
  this._logger.debug("deliciousAPI_call request URL: '" + url + "'");
  request.send(null);
};

/**
 * Call the del.ico.us "posts/all" API method.
 * @param flockIListener aFlockListener (in)
 * @param aAuth (in)
 *    An object with the following two properties:
 *      AString user
 *      AString password
 * @param AString aSeparator (in)
 *    The delimiter to expect in the returned list of tags.
 * @return nothing
 *
 * Except when calling for the first time, "posts/update" should be called
 * before calling this, so that only new posts are returned.
 *
 * @see flockIListener
 */
DeliciousAPI.prototype.postsAll =
function deliciousAPI_postsAll(aFlockListener, aAuth, aSeparator, aUpdateTime) 
{

  var tagSep = " ";
  if (aSeparator) {
    tagSep = aSeparator;
  }

  var api = this;
  var deliciousAPIListener = {
    onError: function deliciousAPI_postsAll_onError(aFlockError) {
      if (aFlockListener) {
        aFlockListener.onError(aFlockError, "error");
      }
    },
    onSuccess: function deliciousAPI_postsAll_onSuccess(aXML) {
      // Validate the nsIDOMDocument response.
      if (!api.isExpectedResponse(aXML, "posts")) {
        api._logger.error("posts/all succeeded, bad xml response");

        var error = CC["@flock.com/error;1"].createInstance(CI.flockIError);

        // FIXME: FAVS_, not OPML_. Clean up flockIError.idl.
        error.errorCode = CI.flockIError.OPML_INVALID_XML;
        if (aXML && aXML.documentElement) {
          api._logger.debug("tagName is: " + aXML.documentElement.tagName);
          error.serviceErrorString = aXML.documentElement.tagName;
        } else {
          api._logger.debug("aXML.documentElement is not defined");
          error.serviceErrorString = "aXML.documentElement is not defined";
        }
        aFlockListener.onError(error, "error");

      } else {

        var result = [];
        var children = aXML.documentElement.childNodes;
        for (var i=0; i<children.length; i++) {
          var child = children.item(i);
          if (child.nodeType != 1) {
            continue;
          }
          child.QueryInterface(CI.nsIDOMElement);
          if (child.tagName != "post") {
            continue;
          }
          if ((child.getAttribute("href") == "about:blank") ||
              (child.getAttribute("href") == ""))
          {
            continue;
          }
          // Use flockXMLDecode because Magnolia does an extra escape of &
          var tags = flockXMLDecode(child.getAttribute("tag"));
          result.push({
            URL: flockXMLDecode(child.getAttribute("href")),
            name: flockXMLDecode(child.getAttribute("description")),
            description: child.getAttribute("extended"),
            tags: tags,
            tag: tags.split(tagSep),
            // "no" is returned for private bookmark,
            // and the attribute is absent for public bookmarks (null)
            shared: (child.getAttribute("shared") !== "no"),
            datevalue: ISO8601DateUtils.parse(child.getAttribute("time")),
            hash: child.getAttribute("hash")
          });
        }
        aFlockListener.onSuccess(result, aUpdateTime);
      }
    }
  };

  this.call("posts/all", {}, deliciousAPIListener, aAuth);
};

/**
 * Call the del.ico.us "posts/update" API method.
 * @param flockIListener aFlockListener (in)
 * @param object aAuth (in)
 *    An object with the following two properties:
 *      AString user
 *      AString password
 * @param Date() aLastUpdate (in)
 *    The last time we retrieved bookmarks from the server.
 * @param AString aSeparator (in)
 *    The delimiter to expect in the returned list of tags.
 * @return nothing
 *
 * @see flockIListener
 */
DeliciousAPI.prototype.postsUpdate =
function deliciousAPI_postsUpdate(aFlockListener, aAuth,
                                  aLastUpdate, aSeparator) {
  var tagSep = " ";
  if (aSeparator) {
    tagSep = aSeparator;
  }

  var api = this;
  var deliciousAPIListener = {
    onError: function deliciousAPI_postsUpdate_onError(aFlockError) {
      if (aFlockListener) {
        aFlockListener.onError(aFlockError, "error");
      }
    },
    onSuccess: function deliciousAPI_postsUpdate_onSuccess(aXML) {
      // Validate the nsIDOMDocument response.
      if (!api.isExpectedResponse(aXML, "update")) {
        api._logger.error('posts/update succeeded, bad xml response');

        var error = CC["@flock.com/error;1"].createInstance(CI.flockIError);

        // FIXME: FAVS_, not OPML_. Clean up flockIError.idl.
        error.errorCode = CI.flockIError.OPML_INVALID_XML;
        if (aXML && aXML.documentElement) {
          api._logger.debug("tagName is: " + aXML.documentElement.tagName);
          error.serviceErrorString = aXML.documentElement.tagName;
        } else {
          api._logger.debug("aXML.documentElement is not defined");
          error.serviceErrorString = "aXML.documentElement is not defined";
        }
        aFlockListener.onError(error, "error");
      } else {
        var lastServerUpdate = aXML.documentElement.getAttribute("time");
        api._logger.debug("Last update on the server  : " + lastServerUpdate);
        api._logger.debug("Last time updates retrieved: " + aLastUpdate);
        if (lastServerUpdate != aLastUpdate) {
          // There is new/updated stuff on the server
          if (api.timer) {
            api.timer.cancel();
          } else {
            api.timer = CC["@mozilla.org/timer;1"]
                        .createInstance(CI.nsITimer);
          }
          // Init a timer to start in 1.3 seconds.
          api.timer.initWithCallback({
            notify: function deliciousAPI_postsUpdateTimer_notify(aTimer) {
              api.postsAll(aFlockListener, aAuth, tagSep, lastServerUpdate);
            }
          }, 1300, CI.nsITimer.TYPE_ONE_SHOT);
        } else {
          // No refresh needed
          if (aFlockListener) {
            aFlockListener.onSuccess(null, "nonew");
          }
        }
      }
    }
  };

  this.call("posts/update", {}, deliciousAPIListener, aAuth);
};

/**
 * Call the del.ico.us "tags/get" API method.
 * @param flockIListener aFlockListener (in)
 * @param object aAuth (in)
 *    An object with the following two properties:
 *      AString user
 *      AString password
 * @return nothing
 *
 * @see flockIListener
 */
DeliciousAPI.prototype.tagsGet =
function deliciousAPI_tagsGet(aFlockListener, aAuth) {

  var api = this;
  var deliciousAPIListener = {
    onError: function deliciousAPI_tagsGet_onError(aFlockError) {
      if (aFlockListener) {
        aFlockListener.onError(aFlockError, "error");
      }
    },
    onSuccess: function deliciousAPI_tagsGet_onSuccess(aXML) {
      // Validate the nsIDOMDocument response.
      if (!api.isExpectedResponse(aXML, "tags")) {
        api._logger.error('tags/get succeeded, bad xml response');

        var error = CC["@flock.com/error;1"].createInstance(CI.flockIError);

        // FIXME: FAVS_, not OPML_. Clean up flockIError.idl.
        error.errorCode = CI.flockIError.OPML_INVALID_XML;
        if (aXML && aXML.documentElement) {
          api._logger.debug("tagName is: " + aXML.documentElement.tagName);
          error.serviceErrorString = aXML.documentElement.tagName;
        } else {
          api._logger.debug("aXML.documentElement is not defined");
          error.serviceErrorString = "aXML.documentElement is not defined";
        }
        aFlockListener.onError(error, "error");

      } else {

        var result = [];
        var children = aXML.documentElement.childNodes;
        for (var i=0; i<children.length; i++) {
          var child = children.item(i);
          if (child.nodeType != 1) {
            continue;
          }
          child.QueryInterface(CI.nsIDOMElement);
          if (child.tagName != "tag") {
            continue;
          }
          result.push({
            tag: child.getAttribute("tag"),
            count: child.getAttribute("count")
          });
        }
        aFlockListener.onSuccess(result, "success");
      }
    }
  };

  this.call("tags/get", {}, deliciousAPIListener, aAuth);
};

/**
 * Helper function to test the API call response.
 * @param nsIDOMDocument aXML (in)
 * @param AString aCompareValue (in)
 * @return boolean
 *    True  - Response indicates successful API call
 *    False - Response is missing, wrong, or reports an error
 */
DeliciousAPI.prototype.isExpectedResponse =
function deliciousAPI_isExpectedResponse(aXML, aCompareValue) {
  // Validate the nsIDOMDocument response.
  if (!aXML ||
      !aXML.documentElement ||
       aXML.documentElement.tagName != aCompareValue)
  {
    return false;
  }
  return true;
};
