/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;

// Modules and services.

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                  "resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                  "resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                  "resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
                                  "resource://gre/modules/Deprecated.jsm");

XPCOMUtils.defineLazyGetter(this, "asyncHistory", function () {
  // Lazily add an history observer when it's actually needed.
  PlacesUtils.history.addObserver(PlacesUtils.livemarks, true);
  return PlacesUtils.asyncHistory;
});

// Constants

// Delay between reloads of consecute livemarks.
const RELOAD_DELAY_MS = 500;
// Expire livemarks after this time.
const EXPIRE_TIME_MS = 3600000; // 1 hour.
// Expire livemarks after this time on error.
const ONERROR_EXPIRE_TIME_MS = 300000; // 5 minutes.

// Livemarks cache.

XPCOMUtils.defineLazyGetter(this, "CACHE_SQL", () => {
  function getAnnoSQLFragment(aAnnoParam) {
    return `SELECT a.content
            FROM moz_items_annos a
            JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id
            WHERE a.item_id = b.id
              AND n.name = ${aAnnoParam}`;
  }

  return `SELECT b.id, b.title, b.parent As parentId, b.position AS 'index',
                 b.guid, b.dateAdded, b.lastModified, p.guid AS parentGuid,
                 ( ${getAnnoSQLFragment(":feedURI_anno")} ) AS feedURI,
                 ( ${getAnnoSQLFragment(":siteURI_anno")} ) AS siteURI
          FROM moz_bookmarks b
          JOIN moz_bookmarks p ON b.parent = p.id
          JOIN moz_items_annos a ON a.item_id = b.id
          JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id
          WHERE b.type = :folder_type
            AND n.name = :feedURI_anno`;
});

XPCOMUtils.defineLazyGetter(this, "gLivemarksCachePromised",
  Task.async(function* () {
    let livemarksMap = new Map();
    let conn = yield PlacesUtils.promiseDBConnection();
    let rows = yield conn.executeCached(CACHE_SQL,
      { folder_type: Ci.nsINavBookmarksService.TYPE_FOLDER,
        feedURI_anno: PlacesUtils.LMANNO_FEEDURI,
        siteURI_anno: PlacesUtils.LMANNO_SITEURI });
    for (let row of rows) {
      let siteURI = row.getResultByName("siteURI");
      let livemark = new Livemark({
        id: row.getResultByName("id"),
        guid: row.getResultByName("guid"),
        title: row.getResultByName("title"),
        parentId: row.getResultByName("parentId"),
        parentGuid: row.getResultByName("parentGuid"),
        index: row.getResultByName("index"),
        dateAdded: row.getResultByName("dateAdded"),
        lastModified: row.getResultByName("lastModified"),
        feedURI: NetUtil.newURI(row.getResultByName("feedURI")),
        siteURI: siteURI ? NetUtil.newURI(siteURI) : null
      });
      livemarksMap.set(livemark.guid, livemark);
    }
    return livemarksMap;
  })
);

/**
 * Convert a Date object to a PRTime (microseconds).
 *
 * @param date
 *        the Date object to convert.
 * @return microseconds from the epoch.
 */
function toPRTime(date) {
  return date * 1000;
}

/**
 * Convert a PRTime to a Date object.
 *
 * @param time
 *        microseconds from the epoch.
 * @return a Date object or undefined if time was not defined.
 */
function toDate(time) {
  return time ? new Date(parseInt(time / 1000)) : undefined;
}

// LivemarkService

function LivemarkService() {
  // Cleanup on shutdown.
  Services.obs.addObserver(this, PlacesUtils.TOPIC_SHUTDOWN, true);

  // Observe bookmarks but don't init the service just for that.
  PlacesUtils.addLazyBookmarkObserver(this, true);
}

LivemarkService.prototype = {
  // This is just an helper for code readability.
  _promiseLivemarksMap: () => gLivemarksCachePromised,

  _reloading: false,
  _startReloadTimer(livemarksMap, forceUpdate, reloaded) {
    if (this._reloadTimer) {
      this._reloadTimer.cancel();
    }
    else {
      this._reloadTimer = Cc["@mozilla.org/timer;1"]
                            .createInstance(Ci.nsITimer);
    }

    this._reloading = true;
    this._reloadTimer.initWithCallback(() => {
      // Find first livemark to be reloaded.
      for (let [ guid, livemark ] of livemarksMap) {
        if (!reloaded.has(guid)) {
          reloaded.add(guid);
          livemark.reload(forceUpdate);
          this._startReloadTimer(livemarksMap, forceUpdate, reloaded);
          return;
        }
      }
      // All livemarks have been reloaded.
      this._reloading = false;
    }, RELOAD_DELAY_MS, Ci.nsITimer.TYPE_ONE_SHOT);
  },

  // nsIObserver

  observe(aSubject, aTopic, aData) {
    if (aTopic == PlacesUtils.TOPIC_SHUTDOWN) {
      if (this._reloadTimer) {
        this._reloading = false;
        this._reloadTimer.cancel();
        delete this._reloadTimer;
      }

      // Stop any ongoing network fetch.
      this._promiseLivemarksMap().then(livemarksMap => {
        for (let livemark of livemarksMap.values()) {
          livemark.terminate();
        }
      });
    }
  },

  // mozIAsyncLivemarks

  addLivemark(aLivemarkInfo) {
    if (!aLivemarkInfo) {
      throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
    }
    let hasParentId = "parentId" in aLivemarkInfo;
    let hasParentGuid = "parentGuid" in aLivemarkInfo;
    let hasIndex = "index" in aLivemarkInfo;
    // Must provide at least non-null parent guid/id, index and feedURI.
    if ((!hasParentId && !hasParentGuid) ||
        (hasParentId && aLivemarkInfo.parentId < 1) ||
        (hasParentGuid &&!/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.parentGuid)) ||
        (hasIndex && aLivemarkInfo.index < Ci.nsINavBookmarksService.DEFAULT_INDEX) ||
        !(aLivemarkInfo.feedURI instanceof Ci.nsIURI) ||
        (aLivemarkInfo.siteURI && !(aLivemarkInfo.siteURI instanceof Ci.nsIURI)) ||
        (aLivemarkInfo.guid && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid))) {
      throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
    }

    return Task.spawn(function* () {
      if (!aLivemarkInfo.parentGuid)
        aLivemarkInfo.parentGuid = yield PlacesUtils.promiseItemGuid(aLivemarkInfo.parentId);

      let livemarksMap = yield this._promiseLivemarksMap();

      // Disallow adding a livemark inside another livemark.
      if (livemarksMap.has(aLivemarkInfo.parentGuid)) {
        throw new Components.Exception("Cannot create a livemark inside a livemark", Cr.NS_ERROR_INVALID_ARG);
      }

      // Create a new livemark.
      let folder = yield PlacesUtils.bookmarks.insert({
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
        parentGuid: aLivemarkInfo.parentGuid,
        title: aLivemarkInfo.title,
        index: aLivemarkInfo.index,
        guid: aLivemarkInfo.guid,
        dateAdded: toDate(aLivemarkInfo.dateAdded) || toDate(aLivemarkInfo.lastModified),
        source: aLivemarkInfo.source,
      });

      // Set feed and site URI annotations.
      let id = yield PlacesUtils.promiseItemId(folder.guid);

      // Create the internal Livemark object.
      let livemark = new Livemark({ id
                                  , title:        folder.title
                                  , parentGuid:   folder.parentGuid
                                  , parentId:     yield PlacesUtils.promiseItemId(folder.parentGuid)
                                  , index:        folder.index
                                  , feedURI:      aLivemarkInfo.feedURI
                                  , siteURI:      aLivemarkInfo.siteURI
                                  , guid:         folder.guid
                                  , dateAdded:    toPRTime(folder.dateAdded)
                                  , lastModified: toPRTime(folder.lastModified)
                                  });

      livemark.writeFeedURI(aLivemarkInfo.feedURI, aLivemarkInfo.source);
      if (aLivemarkInfo.siteURI) {
        livemark.writeSiteURI(aLivemarkInfo.siteURI, aLivemarkInfo.source);
      }

      if (aLivemarkInfo.lastModified) {
        yield PlacesUtils.bookmarks.update({ guid: folder.guid,
                                             lastModified: toDate(aLivemarkInfo.lastModified),
                                             source: aLivemarkInfo.source });
        livemark.lastModified = aLivemarkInfo.lastModified;
      }

      livemarksMap.set(folder.guid, livemark);

      return livemark;
    }.bind(this));
  },

  removeLivemark(aLivemarkInfo) {
    if (!aLivemarkInfo) {
      throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
    }
    // Accept either a guid or an id.
    let hasGuid = "guid" in aLivemarkInfo;
    let hasId = "id" in aLivemarkInfo;
    if ((hasGuid && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) ||
        (hasId && aLivemarkInfo.id < 1) ||
        (!hasId && !hasGuid)) {
      throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
    }

    return Task.spawn(function* () {
      if (!aLivemarkInfo.guid)
        aLivemarkInfo.guid = yield PlacesUtils.promiseItemGuid(aLivemarkInfo.id);

      let livemarksMap = yield this._promiseLivemarksMap();
      if (!livemarksMap.has(aLivemarkInfo.guid))
        throw new Components.Exception("Invalid livemark", Cr.NS_ERROR_INVALID_ARG);

      yield PlacesUtils.bookmarks.remove(aLivemarkInfo.guid,
                                         { source: aLivemarkInfo.source });
    }.bind(this));
  },

  reloadLivemarks(aForceUpdate) {
    // Check if there's a currently running reload, to save some useless work.
    let notWorthRestarting =
      this._forceUpdate || // We're already forceUpdating.
      !aForceUpdate;       // The caller didn't request a forced update.
    if (this._reloading && notWorthRestarting) {
      // Ignore this call.
      return;
    }

    this._promiseLivemarksMap().then(livemarksMap => {
      this._forceUpdate = !!aForceUpdate;
      // Livemarks reloads happen on a timer for performance reasons.
      this._startReloadTimer(livemarksMap, this._forceUpdate, new Set());
    });
  },

  getLivemark(aLivemarkInfo) {
    if (!aLivemarkInfo) {
      throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
    }
    // Accept either a guid or an id.
    let hasGuid = "guid" in aLivemarkInfo;
    let hasId = "id" in aLivemarkInfo;
    if ((hasGuid && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) ||
        (hasId && aLivemarkInfo.id < 1) ||
        (!hasId && !hasGuid)) {
      throw new Components.Exception("Invalid arguments", Cr.NS_ERROR_INVALID_ARG);
    }

    return Task.spawn(function*() {
      if (!aLivemarkInfo.guid)
        aLivemarkInfo.guid = yield PlacesUtils.promiseItemGuid(aLivemarkInfo.id);

      let livemarksMap = yield this._promiseLivemarksMap();
      if (!livemarksMap.has(aLivemarkInfo.guid))
        throw new Components.Exception("Invalid livemark", Cr.NS_ERROR_INVALID_ARG);

      return livemarksMap.get(aLivemarkInfo.guid);
    }.bind(this));
  },

  // nsINavBookmarkObserver

  onBeginUpdateBatch() {},
  onEndUpdateBatch() {},
  onItemVisited() {},
  onItemAdded() {},

  onItemChanged(id, property, isAnno, value, lastModified, itemType, parentId,
                guid, parentGuid) {
    if (itemType != Ci.nsINavBookmarksService.TYPE_FOLDER)
      return;

    this._promiseLivemarksMap().then(livemarksMap => {
      if (livemarksMap.has(guid)) {
        let livemark = livemarksMap.get(guid);
        if (property == "title") {
          livemark.title = value;
        }
        livemark.lastModified = lastModified;
      }
    });
  },

  onItemMoved(id, parentId, oldIndex, newParentId, newIndex, itemType, guid,
              oldParentGuid, newParentGuid) {
    if (itemType != Ci.nsINavBookmarksService.TYPE_FOLDER)
      return;

    this._promiseLivemarksMap().then(livemarksMap => {
      if (livemarksMap.has(guid)) {
        let livemark = livemarksMap.get(guid);
        livemark.parentId = newParentId;
        livemark.parentGuid = newParentGuid;
        livemark.index = newIndex;
      }
    });
  },

  onItemRemoved(id, parentId, index, itemType, uri, guid, parentGuid) {
    if (itemType != Ci.nsINavBookmarksService.TYPE_FOLDER)
      return;

    this._promiseLivemarksMap().then(livemarksMap => {
      if (livemarksMap.has(guid)) {
        let livemark = livemarksMap.get(guid);
        livemark.terminate();
        livemarksMap.delete(guid);
      }
    });
  },

  // nsINavHistoryObserver

  onPageChanged() {},
  onTitleChanged() {},
  onDeleteVisits() {},

  onClearHistory() {
    this._promiseLivemarksMap().then(livemarksMap => {
      for (let livemark of livemarksMap.values()) {
        livemark.updateURIVisitedStatus(null, false);
      }
    });
  },

  onDeleteURI(aURI) {
    this._promiseLivemarksMap().then(livemarksMap => {
      for (let livemark of livemarksMap.values()) {
        livemark.updateURIVisitedStatus(aURI, false);
      }
    });
  },

  onVisit(aURI) {
    this._promiseLivemarksMap().then(livemarksMap => {
      for (let livemark of livemarksMap.values()) {
        livemark.updateURIVisitedStatus(aURI, true);
      }
    });
  },

  // nsISupports

  classID: Components.ID("{dca61eb5-c7cd-4df1-b0fb-d0722baba251}"),

  _xpcom_factory: XPCOMUtils.generateSingletonFactory(LivemarkService),

  QueryInterface: XPCOMUtils.generateQI([
    Ci.mozIAsyncLivemarks
  , Ci.nsINavBookmarkObserver
  , Ci.nsINavHistoryObserver
  , Ci.nsIObserver
  , Ci.nsISupportsWeakReference
  ])
};

// Livemark

/**
 * Object used internally to represent a livemark.
 *
 * @param aLivemarkInfo
 *        Object containing information on the livemark.  If the livemark is
 *        not included in the object, a new livemark will be created.
 *
 * @note terminate() must be invoked before getting rid of this object.
 */
function Livemark(aLivemarkInfo)
{
  this.id = aLivemarkInfo.id;
  this.guid = aLivemarkInfo.guid;
  this.feedURI = aLivemarkInfo.feedURI;
  this.siteURI = aLivemarkInfo.siteURI || null;
  this.title = aLivemarkInfo.title;
  this.parentId = aLivemarkInfo.parentId;
  this.parentGuid = aLivemarkInfo.parentGuid;
  this.index = aLivemarkInfo.index;
  this.dateAdded = aLivemarkInfo.dateAdded;
  this.lastModified = aLivemarkInfo.lastModified;

  this._status = Ci.mozILivemark.STATUS_READY;

  // Hash of resultObservers, hashed by container.
  this._resultObservers = new Map();

  // Sorted array of objects representing livemark children in the form
  // { uri, title, visited }.
  this._children = [];

  // Keeps a separate array of nodes for each requesting container, hashed by
  // the container itself.
  this._nodes = new Map();

  this.loadGroup = null;
  this.expireTime = 0;
}

Livemark.prototype = {
  get status() {
    return this._status;
  },
  set status(val) {
    if (this._status != val) {
      this._status = val;
      this._invalidateRegisteredContainers();
    }
    return this._status;
  },

  writeFeedURI(aFeedURI, aSource) {
    PlacesUtils.annotations
               .setItemAnnotation(this.id, PlacesUtils.LMANNO_FEEDURI,
                                  aFeedURI.spec,
                                  0, PlacesUtils.annotations.EXPIRE_NEVER,
                                  aSource);
    this.feedURI = aFeedURI;
  },

  writeSiteURI(aSiteURI, aSource) {
    if (!aSiteURI) {
      PlacesUtils.annotations.removeItemAnnotation(this.id,
                                                   PlacesUtils.LMANNO_SITEURI,
                                                   aSource)
      this.siteURI = null;
      return;
    }

    // Security check the site URI against the feed URI principal.
    let secMan = Services.scriptSecurityManager;
    let feedPrincipal = secMan.createCodebasePrincipal(this.feedURI, {});
    try {
      secMan.checkLoadURIWithPrincipal(feedPrincipal, aSiteURI,
                                       Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
    }
    catch (ex) {
      return;
    }

    PlacesUtils.annotations
               .setItemAnnotation(this.id, PlacesUtils.LMANNO_SITEURI,
                                  aSiteURI.spec,
                                  0, PlacesUtils.annotations.EXPIRE_NEVER,
                                  aSource);
    this.siteURI = aSiteURI;
  },

  /**
   * Tries to updates the livemark if needed.
   * The update process is asynchronous.
   *
   * @param [optional] aForceUpdate
   *        If true will try to update the livemark even if its contents have
   *        not yet expired.
   */
  updateChildren(aForceUpdate) {
    // Check if the livemark is already updating.
    if (this.status == Ci.mozILivemark.STATUS_LOADING)
      return;

    // Check the TTL/expiration on this, to check if there is no need to update
    // this livemark.
    if (!aForceUpdate && this.children.length && this.expireTime > Date.now())
      return;

    this.status = Ci.mozILivemark.STATUS_LOADING;

    // Setting the status notifies observers that may remove the livemark.
    if (this._terminated)
      return;

    try {
      // Create a load group for the request.  This will allow us to
      // automatically keep track of redirects, so we can always
      // cancel the channel.
      let loadgroup = Cc["@mozilla.org/network/load-group;1"].
                      createInstance(Ci.nsILoadGroup);
      // Creating a CodeBasePrincipal and using it as the loadingPrincipal
      // is *not* desired and is only tolerated within this file.
      // TODO: Find the right OriginAttributes and pass something other
      // than {} to .createCodeBasePrincipal().
      let channel = NetUtil.newChannel({
        uri: this.feedURI,
        loadingPrincipal: Services.scriptSecurityManager.createCodebasePrincipal(this.feedURI, {}),
        securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
        contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_XMLHTTPREQUEST
      }).QueryInterface(Ci.nsIHttpChannel);
      channel.loadGroup = loadgroup;
      channel.loadFlags |= Ci.nsIRequest.LOAD_BACKGROUND |
                           Ci.nsIRequest.LOAD_BYPASS_CACHE;
      channel.requestMethod = "GET";
      channel.setRequestHeader("X-Moz", "livebookmarks", false);

      // Stream the result to the feed parser with this listener
      let listener = new LivemarkLoadListener(this);
      channel.notificationCallbacks = listener;
      channel.asyncOpen2(listener);

      this.loadGroup = loadgroup;
    }
    catch (ex) {
      this.status = Ci.mozILivemark.STATUS_FAILED;
    }
  },

  reload(aForceUpdate) {
    this.updateChildren(aForceUpdate);
  },

  get children() {
    return this._children;
  },
  set children(val) {
    this._children = val;

    // Discard the previous cached nodes, new ones should be generated.
    for (let container of this._resultObservers.keys()) {
      this._nodes.delete(container);
    }

    // Update visited status for each entry.
    for (let child of this._children) {
      asyncHistory.isURIVisited(child.uri, (aURI, aIsVisited) => {
        this.updateURIVisitedStatus(aURI, aIsVisited);
      });
    }

    return this._children;
  },

  _isURIVisited(aURI) {
    return this.children.some(child => child.uri.equals(aURI) && child.visited);
  },

  getNodesForContainer(aContainerNode) {
    if (this._nodes.has(aContainerNode)) {
      return this._nodes.get(aContainerNode);
    }

    let livemark = this;
    let nodes = [];
    let now = Date.now() * 1000;
    for (let child of this.children) {
      // Workaround for bug 449811.
      let localChild = child;
      let node = {
        // The QueryInterface is needed cause aContainerNode is a jsval.
        // This is required to avoid issues with scriptable wrappers that would
        // not allow the view to correctly set expandos.
        get parent() {
          return aContainerNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
        },
        get parentResult() {
          return this.parent.parentResult;
        },
        get uri() {
          return localChild.uri.spec;
        },
        get type() {
          return Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
        },
        get title() {
          return localChild.title;
        },
        get accessCount() {
          return Number(livemark._isURIVisited(NetUtil.newURI(this.uri)));
        },
        get time() {
          return 0;
        },
        get icon() {
          return "";
        },
        get indentLevel() {
          return this.parent.indentLevel + 1;
        },
        get bookmarkIndex() {
            return -1;
        },
        get itemId() {
            return -1;
        },
        get dateAdded() {
          return now;
        },
        get lastModified() {
          return now;
        },
        get tags() {
          return PlacesUtils.tagging.getTagsForURI(NetUtil.newURI(this.uri)).join(", ");
        },
        QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryResultNode])
      };
      nodes.push(node);
    }
    this._nodes.set(aContainerNode, nodes);
    return nodes;
  },

  registerForUpdates(aContainerNode, aResultObserver) {
    this._resultObservers.set(aContainerNode, aResultObserver);
  },

  unregisterForUpdates(aContainerNode) {
    this._resultObservers.delete(aContainerNode);
    this._nodes.delete(aContainerNode);
  },

  _invalidateRegisteredContainers() {
    for (let [ container, observer ] of this._resultObservers) {
      observer.invalidateContainer(container);
    }
  },

  /**
   * Updates the visited status of nodes observing this livemark.
   *
   * @param aURI
   *        If provided will update nodes having the given uri,
   *        otherwise any node.
   * @param aVisitedStatus
   *        Whether the nodes should be set as visited.
   */
  updateURIVisitedStatus(aURI, aVisitedStatus) {
    for (let child of this.children) {
      if (!aURI || child.uri.equals(aURI)) {
        child.visited = aVisitedStatus;
      }
    }

    for (let [ container, observer ] of this._resultObservers) {
      if (this._nodes.has(container)) {
        let nodes = this._nodes.get(container);
        for (let node of nodes) {
          // Workaround for bug 449811.
          let localObserver = observer;
          let localNode = node;
          if (!aURI || node.uri == aURI.spec) {
            Services.tm.mainThread.dispatch(() => {
              localObserver.nodeHistoryDetailsChanged(localNode, 0, aVisitedStatus);
            }, Ci.nsIThread.DISPATCH_NORMAL);
          }
        }
      }
    }
  },

  /**
   * Terminates the livemark entry, cancelling any ongoing load.
   * Must be invoked before destroying the entry.
   */
  terminate() {
    // Avoid handling any updateChildren request from now on.
    this._terminated = true;
    this.abort();
  },

  /**
   * Aborts the livemark loading if needed.
   */
  abort() {
    this.status = Ci.mozILivemark.STATUS_FAILED;
    if (this.loadGroup) {
      this.loadGroup.cancel(Cr.NS_BINDING_ABORTED);
      this.loadGroup = null;
    }
  },

  QueryInterface: XPCOMUtils.generateQI([
    Ci.mozILivemark
  ])
}

// LivemarkLoadListener

/**
 * Object used internally to handle loading a livemark's contents.
 *
 * @param aLivemark
 *        The Livemark that is loading.
 */
function LivemarkLoadListener(aLivemark) {
  this._livemark = aLivemark;
  this._processor = null;
  this._isAborted = false;
  this._ttl = EXPIRE_TIME_MS;
}

LivemarkLoadListener.prototype = {
  abort(aException) {
    if (!this._isAborted) {
      this._isAborted = true;
      this._livemark.abort();
      this._setResourceTTL(ONERROR_EXPIRE_TIME_MS);
    }
  },

  // nsIFeedResultListener
  handleResult(aResult) {
    if (this._isAborted) {
      return;
    }

    try {
      // We need this to make sure the item links are safe
      let feedPrincipal =
        Services.scriptSecurityManager
                .createCodebasePrincipal(this._livemark.feedURI, {});

      // Enforce well-formedness because the existing code does
      if (!aResult || !aResult.doc || aResult.bozo) {
        throw new Components.Exception("", Cr.NS_ERROR_FAILURE);
      }

      let feed = aResult.doc.QueryInterface(Ci.nsIFeed);
      let siteURI = this._livemark.siteURI;
      if (feed.link && (!siteURI || !feed.link.equals(siteURI))) {
        siteURI = feed.link;
        this._livemark.writeSiteURI(siteURI);
      }

      // Insert feed items.
      let livemarkChildren = [];
      for (let i = 0; i < feed.items.length; ++i) {
        let entry = feed.items.queryElementAt(i, Ci.nsIFeedEntry);
        let uri = entry.link || siteURI;
        if (!uri) {
          continue;
        }

        try {
          Services.scriptSecurityManager
                  .checkLoadURIWithPrincipal(feedPrincipal, uri,
                                             Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
        }
        catch (ex) {
          continue;
        }

        let title = entry.title ? entry.title.plainText() : "";
        livemarkChildren.push({ uri: uri, title: title, visited: false });
      }

      this._livemark.children = livemarkChildren;
    }
    catch (ex) {
      this.abort(ex);
    }
    finally {
      this._processor.listener = null;
      this._processor = null;
    }
  },

  onDataAvailable(aRequest, aContext, aInputStream, aSourceOffset, aCount) {
    if (this._processor) {
      this._processor.onDataAvailable(aRequest, aContext, aInputStream,
                                      aSourceOffset, aCount);
    }
  },

  onStartRequest(aRequest, aContext) {
    if (this._isAborted) {
      throw new Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
    }

    let channel = aRequest.QueryInterface(Ci.nsIChannel);
    try {
      // Parse feed data as it comes in
      this._processor = Cc["@mozilla.org/feed-processor;1"].
                        createInstance(Ci.nsIFeedProcessor);
      this._processor.listener = this;
      this._processor.parseAsync(null, channel.URI);
      this._processor.onStartRequest(aRequest, aContext);
    }
    catch (ex) {
      Components.utils.reportError("Livemark Service: feed processor received an invalid channel for " + channel.URI.spec);
      this.abort(ex);
    }
  },

  onStopRequest(aRequest, aContext, aStatus) {
    if (!Components.isSuccessCode(aStatus)) {
      this.abort();
      return;
    }

    // Set an expiration on the livemark, to reloading the data in future.
    try {
      if (this._processor) {
        this._processor.onStopRequest(aRequest, aContext, aStatus);
      }

      // Calculate a new ttl
      let channel = aRequest.QueryInterface(Ci.nsICachingChannel);
      if (channel) {
        let entryInfo = channel.cacheToken.QueryInterface(Ci.nsICacheEntry);
        if (entryInfo) {
          // nsICacheEntry returns value as seconds.
          let expireTime = entryInfo.expirationTime * 1000;
          let nowTime = Date.now();
          // Note, expireTime can be 0, see bug 383538.
          if (expireTime > nowTime) {
            this._setResourceTTL(Math.max((expireTime - nowTime),
                                          EXPIRE_TIME_MS));
            return;
          }
        }
      }
      this._setResourceTTL(EXPIRE_TIME_MS);
    }
    catch (ex) {
      this.abort(ex);
    }
    finally {
      if (this._livemark.status == Ci.mozILivemark.STATUS_LOADING) {
        this._livemark.status = Ci.mozILivemark.STATUS_READY;
      }
      this._livemark.locked = false;
      this._livemark.loadGroup = null;
    }
  },

  _setResourceTTL(aMilliseconds) {
    this._livemark.expireTime = Date.now() + aMilliseconds;
  },

  // nsIInterfaceRequestor
  getInterface(aIID) {
    return this.QueryInterface(aIID);
  },

  // nsISupports
  QueryInterface: XPCOMUtils.generateQI([
    Ci.nsIFeedResultListener
  , Ci.nsIStreamListener
  , Ci.nsIRequestObserver
  , Ci.nsIInterfaceRequestor
  ])
}

this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LivemarkService]);
