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

#include "nsNavBookmarks.h"

#include "nsNavHistory.h"
#include "nsPlacesMacros.h"
#include "Helpers.h"

#include "nsAppDirectoryServiceDefs.h"
#include "nsITaggingService.h"
#include "nsNetUtil.h"
#include "nsIProtocolHandler.h"
#include "nsIObserverService.h"
#include "nsUnicharUtils.h"
#include "nsPrintfCString.h"
#include "nsQueryObject.h"
#include "mozIStorageValueArray.h"
#include "mozilla/Preferences.h"
#include "mozilla/ProfilerLabels.h"
#include "mozilla/storage.h"
#include "mozilla/dom/PlacesBookmarkAddition.h"
#include "mozilla/dom/PlacesBookmarkRemoved.h"
#include "mozilla/dom/PlacesBookmarkTags.h"
#include "mozilla/dom/PlacesBookmarkTime.h"
#include "mozilla/dom/PlacesBookmarkTitle.h"
#include "mozilla/dom/PlacesObservers.h"
#include "mozilla/dom/PlacesVisit.h"

using namespace mozilla;

// These columns sit to the right of the kGetInfoIndex_* columns.
const int32_t nsNavBookmarks::kGetChildrenIndex_Guid = 18;
const int32_t nsNavBookmarks::kGetChildrenIndex_Position = 19;
const int32_t nsNavBookmarks::kGetChildrenIndex_Type = 20;
const int32_t nsNavBookmarks::kGetChildrenIndex_PlaceID = 21;
const int32_t nsNavBookmarks::kGetChildrenIndex_SyncStatus = 22;

using namespace mozilla::dom;
using namespace mozilla::places;

extern "C" {

// Returns the total number of Sync changes recorded since Places startup for
// all bookmarks. This function uses C linkage because it's called from the
// Rust synced bookmarks mirror, on the storage thread. Using `get_service` to
// access the bookmarks service from Rust trips a thread-safety assertion, so
// we can't use `nsNavBookmarks::GetTotalSyncChanges`.
int64_t NS_NavBookmarksTotalSyncChanges() {
  return nsNavBookmarks::sTotalSyncChanges;
}

}  // extern "C"

PLACES_FACTORY_SINGLETON_IMPLEMENTATION(nsNavBookmarks, gBookmarksService)

namespace {

// Returns the sync change counter increment for a change source constant.
inline int64_t DetermineSyncChangeDelta(uint16_t aSource) {
  return aSource == nsINavBookmarksService::SOURCE_SYNC ? 0 : 1;
}

// Returns the sync status for a new item inserted by a change source.
inline int32_t DetermineInitialSyncStatus(uint16_t aSource) {
  if (aSource == nsINavBookmarksService::SOURCE_SYNC) {
    return nsINavBookmarksService::SYNC_STATUS_NORMAL;
  }
  if (aSource == nsINavBookmarksService::SOURCE_RESTORE_ON_STARTUP) {
    return nsINavBookmarksService::SYNC_STATUS_UNKNOWN;
  }
  return nsINavBookmarksService::SYNC_STATUS_NEW;
}

// Indicates whether an item has been uploaded to the server and
// needs a tombstone on deletion.
inline bool NeedsTombstone(const BookmarkData& aBookmark) {
  return aBookmark.syncStatus == nsINavBookmarksService::SYNC_STATUS_NORMAL;
}

inline nsresult GetTags(nsIURI* aURI, nsTArray<nsString>& aResult) {
  nsresult rv;
  nsCOMPtr<nsITaggingService> taggingService =
      do_GetService("@mozilla.org/browser/tagging-service;1", &rv);

  if (NS_FAILED(rv)) {
    return rv;
  }

  return taggingService->GetTagsForURI(aURI, aResult);
}

}  // namespace

nsNavBookmarks::nsNavBookmarks() : mCanNotify(false) {
  NS_ASSERTION(!gBookmarksService,
               "Attempting to create two instances of the service!");
  gBookmarksService = this;
}

nsNavBookmarks::~nsNavBookmarks() {
  NS_ASSERTION(gBookmarksService == this,
               "Deleting a non-singleton instance of the service");
  if (gBookmarksService == this) gBookmarksService = nullptr;
}

NS_IMPL_ISUPPORTS(nsNavBookmarks, nsINavBookmarksService, nsIObserver,
                  nsISupportsWeakReference)

Atomic<int64_t> nsNavBookmarks::sLastInsertedItemId(0);

void  // static
nsNavBookmarks::StoreLastInsertedId(const nsACString& aTable,
                                    const int64_t aLastInsertedId) {
  MOZ_ASSERT(aTable.EqualsLiteral("moz_bookmarks"));
  sLastInsertedItemId = aLastInsertedId;
}

Atomic<int64_t> nsNavBookmarks::sTotalSyncChanges(0);

void  // static
nsNavBookmarks::NoteSyncChange() {
  sTotalSyncChanges++;
}

nsresult nsNavBookmarks::Init() {
  mDB = Database::GetDatabase();
  NS_ENSURE_STATE(mDB);

  nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
  if (os) {
    (void)os->AddObserver(this, TOPIC_PLACES_CONNECTION_CLOSED, true);
  }

  mCanNotify = true;

  // DO NOT PUT STUFF HERE that can fail. See observer comment above.

  return NS_OK;
}

nsresult nsNavBookmarks::AdjustIndices(int64_t aFolderId, int32_t aStartIndex,
                                       int32_t aEndIndex, int32_t aDelta) {
  NS_ASSERTION(aStartIndex >= 0 && aStartIndex <= aEndIndex, "Bad indices");

  nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
      "UPDATE moz_bookmarks SET position = position + :delta "
      "WHERE parent = :parent "
      "AND position BETWEEN :from_index AND :to_index");
  NS_ENSURE_STATE(stmt);
  mozStorageStatementScoper scoper(stmt);

  nsresult rv = stmt->BindInt32ByName("delta"_ns, aDelta);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = stmt->BindInt64ByName("parent"_ns, aFolderId);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = stmt->BindInt32ByName("from_index"_ns, aStartIndex);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = stmt->BindInt32ByName("to_index"_ns, aEndIndex);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = stmt->Execute();
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

nsresult nsNavBookmarks::AdjustSeparatorsSyncCounter(int64_t aFolderId,
                                                     int32_t aStartIndex,
                                                     int64_t aSyncChangeDelta) {
  MOZ_ASSERT(aStartIndex >= 0, "Bad start position");
  if (!aSyncChangeDelta) {
    return NS_OK;
  }

  nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
      "UPDATE moz_bookmarks SET syncChangeCounter = syncChangeCounter + :delta "
      "WHERE parent = :parent AND position >= :start_index "
      "AND type = :item_type ");
  NS_ENSURE_STATE(stmt);
  mozStorageStatementScoper scoper(stmt);

  nsresult rv = stmt->BindInt64ByName("delta"_ns, aSyncChangeDelta);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = stmt->BindInt64ByName("parent"_ns, aFolderId);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = stmt->BindInt32ByName("start_index"_ns, aStartIndex);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = stmt->BindInt32ByName("item_type"_ns, TYPE_SEPARATOR);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = stmt->Execute();
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

NS_IMETHODIMP
nsNavBookmarks::GetTagsFolder(int64_t* aRoot) {
  int64_t id = mDB->GetTagsFolderId();
  NS_ENSURE_TRUE(id > 0, NS_ERROR_UNEXPECTED);
  *aRoot = id;
  return NS_OK;
}

NS_IMETHODIMP
nsNavBookmarks::GetTotalSyncChanges(int64_t* aTotalSyncChanges) {
  *aTotalSyncChanges = sTotalSyncChanges;
  return NS_OK;
}

nsresult nsNavBookmarks::InsertBookmarkInDB(
    int64_t aPlaceId, enum ItemType aItemType, int64_t aParentId,
    int32_t aIndex, const nsACString& aTitle, PRTime aDateAdded,
    PRTime aLastModified, const nsACString& aParentGuid, int64_t aGrandParentId,
    nsIURI* aURI, uint16_t aSource, int64_t* _itemId, nsACString& _guid) {
  // Check for a valid itemId.
  MOZ_ASSERT(_itemId && (*_itemId == -1 || *_itemId > 0));
  // Check for a valid placeId.
  MOZ_ASSERT(aPlaceId && (aPlaceId == -1 || aPlaceId > 0));

  nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
      "INSERT INTO moz_bookmarks "
      "(id, fk, type, parent, position, title, "
      "dateAdded, lastModified, guid, syncStatus, syncChangeCounter) "
      "VALUES (:item_id, :page_id, :item_type, :parent, :item_index, "
      ":item_title, :date_added, :last_modified, "
      ":item_guid, :sync_status, :change_counter)");
  NS_ENSURE_STATE(stmt);
  mozStorageStatementScoper scoper(stmt);

  nsresult rv;
  if (*_itemId != -1)
    rv = stmt->BindInt64ByName("item_id"_ns, *_itemId);
  else
    rv = stmt->BindNullByName("item_id"_ns);
  NS_ENSURE_SUCCESS(rv, rv);

  if (aPlaceId != -1)
    rv = stmt->BindInt64ByName("page_id"_ns, aPlaceId);
  else
    rv = stmt->BindNullByName("page_id"_ns);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = stmt->BindInt32ByName("item_type"_ns, aItemType);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = stmt->BindInt64ByName("parent"_ns, aParentId);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = stmt->BindInt32ByName("item_index"_ns, aIndex);
  NS_ENSURE_SUCCESS(rv, rv);

  if (aTitle.IsEmpty())
    rv = stmt->BindNullByName("item_title"_ns);
  else
    rv = stmt->BindUTF8StringByName("item_title"_ns, aTitle);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = stmt->BindInt64ByName("date_added"_ns, aDateAdded);
  NS_ENSURE_SUCCESS(rv, rv);

  if (aLastModified) {
    rv = stmt->BindInt64ByName("last_modified"_ns, aLastModified);
  } else {
    rv = stmt->BindInt64ByName("last_modified"_ns, aDateAdded);
  }
  NS_ENSURE_SUCCESS(rv, rv);

  // Could use IsEmpty because our callers check for GUID validity,
  // but it doesn't hurt.
  bool hasExistingGuid = _guid.Length() == 12;
  if (hasExistingGuid) {
    MOZ_ASSERT(IsValidGUID(_guid));
    rv = stmt->BindUTF8StringByName("item_guid"_ns, _guid);
    NS_ENSURE_SUCCESS(rv, rv);
  } else {
    nsAutoCString guid;
    rv = GenerateGUID(guid);
    NS_ENSURE_SUCCESS(rv, rv);
    rv = stmt->BindUTF8StringByName("item_guid"_ns, guid);
    NS_ENSURE_SUCCESS(rv, rv);
    _guid.Assign(guid);
  }

  int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);
  rv = stmt->BindInt64ByName("change_counter"_ns, syncChangeDelta);
  NS_ENSURE_SUCCESS(rv, rv);

  uint16_t syncStatus = DetermineInitialSyncStatus(aSource);
  rv = stmt->BindInt32ByName("sync_status"_ns, syncStatus);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = stmt->Execute();
  NS_ENSURE_SUCCESS(rv, rv);

  // Remove stale tombstones if we're reinserting an item.
  if (hasExistingGuid) {
    rv = RemoveTombstone(_guid);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  if (*_itemId == -1) {
    *_itemId = sLastInsertedItemId;
  }

  if (aParentId > 0) {
    // Update last modified date of the ancestors.
    // TODO (bug 408991): Doing this for all ancestors would be slow without a
    //                    nested tree, so for now update only the parent.
    rv = SetItemDateInternal(LAST_MODIFIED, syncChangeDelta, aParentId,
                             aDateAdded);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  int64_t tagsRootId = mDB->GetTagsFolderId();
  bool isTagging = aGrandParentId == tagsRootId;
  if (isTagging) {
    // If we're tagging a bookmark, increment the change counter for all
    // bookmarks with the URI.
    rv = AddSyncChangesForBookmarksWithURI(aURI, syncChangeDelta);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  // Mark all affected separators as changed
  rv = AdjustSeparatorsSyncCounter(aParentId, aIndex + 1, syncChangeDelta);
  NS_ENSURE_SUCCESS(rv, rv);

  // Add a cache entry since we know everything about this bookmark.
  BookmarkData bookmark;
  bookmark.id = *_itemId;
  bookmark.guid.Assign(_guid);
  if (!aTitle.IsEmpty()) {
    bookmark.title.Assign(aTitle);
  }
  bookmark.position = aIndex;
  bookmark.placeId = aPlaceId;
  bookmark.parentId = aParentId;
  bookmark.type = aItemType;
  bookmark.dateAdded = aDateAdded;
  if (aLastModified)
    bookmark.lastModified = aLastModified;
  else
    bookmark.lastModified = aDateAdded;
  if (aURI) {
    rv = aURI->GetSpec(bookmark.url);
    NS_ENSURE_SUCCESS(rv, rv);
  }
  bookmark.parentGuid = aParentGuid;
  bookmark.grandParentId = aGrandParentId;
  bookmark.syncStatus = syncStatus;

  return NS_OK;
}

NS_IMETHODIMP
nsNavBookmarks::InsertBookmark(int64_t aFolder, nsIURI* aURI, int32_t aIndex,
                               const nsACString& aTitle,
                               const nsACString& aGUID, uint16_t aSource,
                               int64_t* aNewBookmarkId) {
  NS_ENSURE_ARG(aURI);
  NS_ENSURE_ARG_POINTER(aNewBookmarkId);
  NS_ENSURE_ARG_MIN(aIndex, nsINavBookmarksService::DEFAULT_INDEX);

  if (!aGUID.IsEmpty() && !IsValidGUID(aGUID)) return NS_ERROR_INVALID_ARG;

  mozStorageTransaction transaction(mDB->MainConn(), false);

  // XXX Handle the error, bug 1696133.
  (void)NS_WARN_IF(NS_FAILED(transaction.Start()));

  nsNavHistory* history = nsNavHistory::GetHistoryService();
  NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
  int64_t placeId;
  nsAutoCString placeGuid;
  nsresult rv = history->GetOrCreateIdForPage(aURI, &placeId, placeGuid);
  NS_ENSURE_SUCCESS(rv, rv);

  // Get the correct index for insertion.  This also ensures the parent exists.
  int32_t index, folderCount;
  int64_t grandParentId;
  nsAutoCString folderGuid;
  rv = FetchFolderInfo(aFolder, &folderCount, folderGuid, &grandParentId);
  NS_ENSURE_SUCCESS(rv, rv);
  if (aIndex == nsINavBookmarksService::DEFAULT_INDEX ||
      aIndex >= folderCount) {
    index = folderCount;
  } else {
    index = aIndex;
    // Create space for the insertion.
    rv = AdjustIndices(aFolder, index, INT32_MAX, 1);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  *aNewBookmarkId = -1;
  PRTime dateAdded = RoundedPRNow();
  nsAutoCString guid(aGUID);
  nsCString title;
  TruncateTitle(aTitle, title);

  rv = InsertBookmarkInDB(placeId, BOOKMARK, aFolder, index, title, dateAdded,
                          0, folderGuid, grandParentId, aURI, aSource,
                          aNewBookmarkId, guid);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = transaction.Commit();
  NS_ENSURE_SUCCESS(rv, rv);

  if (!mCanNotify) {
    return NS_OK;
  }

  Sequence<OwningNonNull<PlacesEvent>> notifications;
  nsAutoCString utf8spec;
  aURI->GetSpec(utf8spec);
  int64_t tagsRootId = mDB->GetTagsFolderId();

  RefPtr<PlacesBookmarkAddition> bookmark = new PlacesBookmarkAddition();
  bookmark->mItemType = TYPE_BOOKMARK;
  bookmark->mId = *aNewBookmarkId;
  bookmark->mParentId = aFolder;
  bookmark->mIndex = index;
  bookmark->mUrl.Assign(NS_ConvertUTF8toUTF16(utf8spec));
  bookmark->mTitle.Assign(NS_ConvertUTF8toUTF16(title));
  bookmark->mDateAdded = dateAdded / 1000;
  bookmark->mGuid.Assign(guid);
  bookmark->mParentGuid.Assign(folderGuid);
  bookmark->mSource = aSource;
  bookmark->mIsTagging = grandParentId == tagsRootId;

  nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
      "SELECT "
      "  h.frecency, "
      "  h.hidden, "
      "  h.visit_count, "
      "  h.last_visit_date, "
      "  (SELECT group_concat(p.title ORDER BY p.title) "
      "    FROM moz_bookmarks b "
      "    JOIN moz_bookmarks p ON p.id = b.parent "
      "    JOIN moz_bookmarks g ON g.id = p.parent "
      "    WHERE g.guid = " SQL_QUOTE(TAGS_ROOT_GUID)
      "      AND b.fk = h.id "
      "  ) AS tags, "
      "  t.guid, t.id, t.title "
      "FROM moz_places h "
      "LEFT JOIN moz_bookmarks t ON t.guid = target_folder_guid(h.url) "
      "WHERE h.id = :id");
  NS_ENSURE_STATE(stmt);
  mozStorageStatementScoper scoper(stmt);
  rv = stmt->BindInt64ByName("id"_ns, placeId);
  NS_ENSURE_SUCCESS(rv, rv);

  bool exists;
  if (NS_SUCCEEDED(stmt->ExecuteStep(&exists)) && exists) {
    int32_t frecency;
    rv = stmt->GetInt32(0, &frecency);
    NS_ENSURE_SUCCESS(rv, rv);
    bookmark->mFrecency = frecency;
    int32_t hidden;
    rv = stmt->GetInt32(1, &hidden);
    NS_ENSURE_SUCCESS(rv, rv);
    bookmark->mHidden = !!hidden;
    int32_t visitCount;
    rv = stmt->GetInt32(2, &visitCount);
    NS_ENSURE_SUCCESS(rv, rv);
    bookmark->mVisitCount = visitCount;

    bool isLastVisitDateNull;
    rv = stmt->GetIsNull(3, &isLastVisitDateNull);
    NS_ENSURE_SUCCESS(rv, rv);
    if (isLastVisitDateNull) {
      bookmark->mLastVisitDate.SetNull();
    } else {
      int64_t lastVisitDate;
      rv = stmt->GetInt64(3, &lastVisitDate);
      NS_ENSURE_SUCCESS(rv, rv);
      bookmark->mLastVisitDate = lastVisitDate;
    }

    nsString tags;
    rv = stmt->GetString(4, tags);
    NS_ENSURE_SUCCESS(rv, rv);
    bookmark->mTags.Assign(tags);

    bool isTargetFolderNull;
    rv = stmt->GetIsNull(5, &isTargetFolderNull);
    NS_ENSURE_SUCCESS(rv, rv);
    if (!isTargetFolderNull) {
      nsCString targetFolderGuid;
      rv = stmt->GetUTF8String(5, targetFolderGuid);
      NS_ENSURE_SUCCESS(rv, rv);
      bookmark->mTargetFolderGuid.Assign(targetFolderGuid);

      int64_t targetFolderItemId = -1;
      rv = stmt->GetInt64(6, &targetFolderItemId);
      NS_ENSURE_SUCCESS(rv, rv);
      bookmark->mTargetFolderItemId = targetFolderItemId;

      nsString targetFolderTitle;
      rv = stmt->GetString(7, targetFolderTitle);
      NS_ENSURE_SUCCESS(rv, rv);
      bookmark->mTargetFolderTitle.Assign(targetFolderTitle);
    } else {
      bookmark->mTargetFolderGuid.SetIsVoid(true);
      bookmark->mTargetFolderItemId = -1;
      bookmark->mTargetFolderTitle.SetIsVoid(true);
    }
  } else {
    MOZ_ASSERT(false);
    bookmark->mTags.SetIsVoid(true);
    bookmark->mFrecency = 0;
    bookmark->mHidden = false;
    bookmark->mVisitCount = 0;
    bookmark->mLastVisitDate.SetNull();
    bookmark->mTargetFolderGuid.SetIsVoid(true);
    bookmark->mTargetFolderItemId = -1;
    bookmark->mTargetFolderTitle.SetIsVoid(true);
  }

  bool success = !!notifications.AppendElement(bookmark.forget(), fallible);
  MOZ_RELEASE_ASSERT(success);

  // If the bookmark has been added to a tag container, notify all
  // bookmark-folder result nodes which contain a bookmark for the new
  // bookmark's url.
  if (grandParentId == tagsRootId) {
    // Notify a tags change to all bookmarks for this URI.
    nsTArray<BookmarkData> bookmarks;
    rv = GetBookmarksForURI(aURI, bookmarks);
    NS_ENSURE_SUCCESS(rv, rv);

    nsTArray<nsString> tags;
    rv = GetTags(aURI, tags);
    NS_ENSURE_SUCCESS(rv, rv);

    for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
      // Check that bookmarks doesn't include the current tag itemId.
      MOZ_ASSERT(bookmarks[i].id != *aNewBookmarkId);
      RefPtr<PlacesBookmarkTags> tagsChanged = new PlacesBookmarkTags();
      tagsChanged->mId = bookmarks[i].id;
      tagsChanged->mItemType = TYPE_BOOKMARK;
      tagsChanged->mUrl.Assign(NS_ConvertUTF8toUTF16(utf8spec));
      tagsChanged->mGuid = bookmarks[i].guid;
      tagsChanged->mParentGuid = bookmarks[i].parentGuid;
      tagsChanged->mTags.Assign(tags);
      tagsChanged->mLastModified = bookmarks[i].lastModified / 1000;
      tagsChanged->mSource = aSource;
      tagsChanged->mIsTagging = false;
      success = !!notifications.AppendElement(tagsChanged.forget(), fallible);
      MOZ_RELEASE_ASSERT(success);
    }
  }

  PlacesObservers::NotifyListeners(notifications);

  return NS_OK;
}

NS_IMETHODIMP
nsNavBookmarks::RemoveItem(int64_t aItemId, uint16_t aSource) {
  AUTO_PROFILER_LABEL("nsNavBookmarks::RemoveItem", OTHER);

  BookmarkData bookmark;
  nsresult rv = FetchItemInfo(aItemId, bookmark);
  NS_ENSURE_SUCCESS(rv, rv);
  // Check we're not trying to remove a root.
  NS_ENSURE_ARG(bookmark.parentId > 0 && bookmark.grandParentId > 0);

  mozStorageTransaction transaction(mDB->MainConn(), false);

  // XXX Handle the error, bug 1696133.
  (void)NS_WARN_IF(NS_FAILED(transaction.Start()));

  if (bookmark.type == TYPE_FOLDER) {
    // Remove all of the folder's children.
    rv = RemoveFolderChildren(bookmark.id, aSource);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  nsCOMPtr<mozIStorageStatement> stmt =
      mDB->GetStatement("DELETE FROM moz_bookmarks WHERE id = :item_id");
  NS_ENSURE_STATE(stmt);
  mozStorageStatementScoper scoper(stmt);

  rv = stmt->BindInt64ByName("item_id"_ns, bookmark.id);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = stmt->Execute();
  NS_ENSURE_SUCCESS(rv, rv);

  // Fix indices in the parent.
  if (bookmark.position != DEFAULT_INDEX) {
    rv = AdjustIndices(bookmark.parentId, bookmark.position + 1, INT32_MAX, -1);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);

  // Add a tombstone for synced items.
  if (syncChangeDelta) {
    rv = InsertTombstone(bookmark);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  bookmark.lastModified = RoundedPRNow();
  rv = SetItemDateInternal(LAST_MODIFIED, syncChangeDelta, bookmark.parentId,
                           bookmark.lastModified);
  NS_ENSURE_SUCCESS(rv, rv);

  // Mark all affected separators as changed
  rv = AdjustSeparatorsSyncCounter(bookmark.parentId, bookmark.position,
                                   syncChangeDelta);
  NS_ENSURE_SUCCESS(rv, rv);

  int64_t tagsRootId = mDB->GetTagsFolderId();
  if (bookmark.grandParentId == tagsRootId) {
    // If we're removing a tag, increment the change counter for all bookmarks
    // with the URI.
    rv = AddSyncChangesForBookmarksWithURL(bookmark.url, syncChangeDelta);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  rv = transaction.Commit();
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIURI> uri;
  if (bookmark.type == TYPE_BOOKMARK) {
    // A broken url should not interrupt the removal process.
    (void)NS_NewURI(getter_AddRefs(uri), bookmark.url);
    // We cannot assert since some automated tests are checking this path.
    NS_WARNING_ASSERTION(uri, "Invalid URI in RemoveItem");
  }

  if (!mCanNotify) {
    return NS_OK;
  }

  Sequence<OwningNonNull<PlacesEvent>> notifications;
  RefPtr<PlacesBookmarkRemoved> bookmarkRef = new PlacesBookmarkRemoved();
  bookmarkRef->mItemType = bookmark.type;
  bookmarkRef->mId = bookmark.id;
  bookmarkRef->mParentId = bookmark.parentId;
  bookmarkRef->mIndex = bookmark.position;
  if (bookmark.type == TYPE_BOOKMARK) {
    bookmarkRef->mUrl.Assign(NS_ConvertUTF8toUTF16(bookmark.url));
  }
  bookmarkRef->mTitle.Assign(NS_ConvertUTF8toUTF16(bookmark.title));
  bookmarkRef->mGuid.Assign(bookmark.guid);
  bookmarkRef->mParentGuid.Assign(bookmark.parentGuid);
  bookmarkRef->mSource = aSource;
  bookmarkRef->mIsTagging =
      bookmark.parentId == tagsRootId || bookmark.grandParentId == tagsRootId;
  bookmarkRef->mIsDescendantRemoval = false;
  bool success = !!notifications.AppendElement(bookmarkRef.forget(), fallible);
  MOZ_RELEASE_ASSERT(success);

  if (bookmark.type == TYPE_BOOKMARK && bookmark.grandParentId == tagsRootId &&
      uri) {
    // If the removed bookmark was child of a tag container, notify a tags
    // change to all bookmarks for this URI.
    nsTArray<BookmarkData> bookmarks;
    rv = GetBookmarksForURI(uri, bookmarks);
    NS_ENSURE_SUCCESS(rv, rv);

    nsAutoCString utf8spec;
    uri->GetSpec(utf8spec);

    nsTArray<nsString> tags;
    rv = GetTags(uri, tags);
    NS_ENSURE_SUCCESS(rv, rv);

    for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
      RefPtr<PlacesBookmarkTags> tagsChanged = new PlacesBookmarkTags();
      tagsChanged->mId = bookmarks[i].id;
      tagsChanged->mItemType = TYPE_BOOKMARK;
      tagsChanged->mUrl.Assign(NS_ConvertUTF8toUTF16(utf8spec));
      tagsChanged->mGuid = bookmarks[i].guid;
      tagsChanged->mParentGuid = bookmarks[i].parentGuid;
      tagsChanged->mTags.Assign(tags);
      tagsChanged->mLastModified = bookmarks[i].lastModified / 1000;
      tagsChanged->mSource = aSource;
      tagsChanged->mIsTagging = false;
      success = !!notifications.AppendElement(tagsChanged.forget(), fallible);
      MOZ_RELEASE_ASSERT(success);
    }
  }

  PlacesObservers::NotifyListeners(notifications);

  return NS_OK;
}

NS_IMETHODIMP
nsNavBookmarks::CreateFolder(int64_t aParent, const nsACString& aTitle,
                             int32_t aIndex, const nsACString& aGUID,
                             uint16_t aSource, int64_t* aNewFolderId) {
  // NOTE: aParent can be null for root creation, so not checked
  NS_ENSURE_ARG_POINTER(aNewFolderId);
  NS_ENSURE_ARG_MIN(aIndex, nsINavBookmarksService::DEFAULT_INDEX);
  if (!aGUID.IsEmpty() && !IsValidGUID(aGUID)) return NS_ERROR_INVALID_ARG;

  // Get the correct index for insertion.  This also ensures the parent exists.
  int32_t index = aIndex, folderCount;
  int64_t grandParentId;
  nsAutoCString folderGuid;
  nsresult rv =
      FetchFolderInfo(aParent, &folderCount, folderGuid, &grandParentId);
  NS_ENSURE_SUCCESS(rv, rv);

  mozStorageTransaction transaction(mDB->MainConn(), false);

  // XXX Handle the error, bug 1696133.
  (void)NS_WARN_IF(NS_FAILED(transaction.Start()));

  if (aIndex == nsINavBookmarksService::DEFAULT_INDEX ||
      aIndex >= folderCount) {
    index = folderCount;
  } else {
    // Create space for the insertion.
    rv = AdjustIndices(aParent, index, INT32_MAX, 1);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  *aNewFolderId = -1;
  PRTime dateAdded = RoundedPRNow();
  nsAutoCString guid(aGUID);
  nsCString title;
  TruncateTitle(aTitle, title);

  rv = InsertBookmarkInDB(-1, FOLDER, aParent, index, title, dateAdded, 0,
                          folderGuid, grandParentId, nullptr, aSource,
                          aNewFolderId, guid);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = transaction.Commit();
  NS_ENSURE_SUCCESS(rv, rv);

  int64_t tagsRootId = mDB->GetTagsFolderId();

  if (mCanNotify) {
    Sequence<OwningNonNull<PlacesEvent>> events;
    RefPtr<PlacesBookmarkAddition> folder = new PlacesBookmarkAddition();
    folder->mItemType = TYPE_FOLDER;
    folder->mId = *aNewFolderId;
    folder->mParentId = aParent;
    folder->mIndex = index;
    folder->mTitle.Assign(NS_ConvertUTF8toUTF16(title));
    folder->mDateAdded = dateAdded / 1000;
    folder->mGuid.Assign(guid);
    folder->mParentGuid.Assign(folderGuid);
    folder->mSource = aSource;
    folder->mIsTagging = aParent == tagsRootId;
    folder->mTags.SetIsVoid(true);
    folder->mFrecency = 0;
    folder->mHidden = false;
    folder->mVisitCount = 0;
    folder->mLastVisitDate.SetNull();
    folder->mTargetFolderGuid.SetIsVoid(true);
    folder->mTargetFolderItemId = -1;
    folder->mTargetFolderTitle.SetIsVoid(true);
    bool success = !!events.AppendElement(folder.forget(), fallible);
    MOZ_RELEASE_ASSERT(success);

    PlacesObservers::NotifyListeners(events);
  }

  return NS_OK;
}

nsresult nsNavBookmarks::GetDescendantChildren(
    int64_t aFolderId, const nsACString& aFolderGuid, int64_t aGrandParentId,
    nsTArray<BookmarkData>& aFolderChildrenArray) {
  // New children will be added from this index on.
  uint32_t startIndex = aFolderChildrenArray.Length();
  nsresult rv;
  {
    // Collect children informations.
    // Select all children of a given folder, sorted by position.
    // This is a LEFT JOIN because not all bookmarks types have a place.
    // We construct a result where the first columns exactly match
    // kGetInfoIndex_* order, and additionally contains columns for position,
    // item_child, and folder_child from moz_bookmarks.
    nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
        "SELECT h.id, h.url, b.title, h.rev_host, h.visit_count, "
        "h.last_visit_date, null, b.id, b.dateAdded, b.lastModified, "
        "b.parent, null, h.frecency, h.hidden, h.guid, null, null, null, "
        "b.guid, b.position, b.type, b.fk, b.syncStatus "
        "FROM moz_bookmarks b "
        "LEFT JOIN moz_places h ON b.fk = h.id "
        "WHERE b.parent = :parent "
        "ORDER BY b.position ASC");
    NS_ENSURE_STATE(stmt);
    mozStorageStatementScoper scoper(stmt);

    rv = stmt->BindInt64ByName("parent"_ns, aFolderId);
    NS_ENSURE_SUCCESS(rv, rv);

    bool hasMore;
    while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
      BookmarkData child;
      rv = stmt->GetInt64(nsNavHistory::kGetInfoIndex_ItemId, &child.id);
      NS_ENSURE_SUCCESS(rv, rv);
      child.parentId = aFolderId;
      child.grandParentId = aGrandParentId;
      child.parentGuid = aFolderGuid;
      rv = stmt->GetInt32(kGetChildrenIndex_Type, &child.type);
      NS_ENSURE_SUCCESS(rv, rv);
      rv = stmt->GetInt64(kGetChildrenIndex_PlaceID, &child.placeId);
      NS_ENSURE_SUCCESS(rv, rv);
      rv = stmt->GetInt32(kGetChildrenIndex_Position, &child.position);
      NS_ENSURE_SUCCESS(rv, rv);
      rv = stmt->GetUTF8String(kGetChildrenIndex_Guid, child.guid);
      NS_ENSURE_SUCCESS(rv, rv);
      rv = stmt->GetInt32(kGetChildrenIndex_SyncStatus, &child.syncStatus);
      NS_ENSURE_SUCCESS(rv, rv);

      if (child.type == TYPE_BOOKMARK) {
        rv = stmt->GetUTF8String(nsNavHistory::kGetInfoIndex_URL, child.url);
        NS_ENSURE_SUCCESS(rv, rv);
      }

      bool isNull;
      rv = stmt->GetIsNull(nsNavHistory::kGetInfoIndex_Title, &isNull);
      NS_ENSURE_SUCCESS(rv, rv);
      if (!isNull) {
        rv =
            stmt->GetUTF8String(nsNavHistory::kGetInfoIndex_Title, child.title);
        NS_ENSURE_SUCCESS(rv, rv);
      }

      // Append item to children's array.
      aFolderChildrenArray.AppendElement(child);
    }
  }

  // Recursively call GetDescendantChildren for added folders.
  // We start at startIndex since previous folders are checked
  // by previous calls to this method.
  uint32_t childCount = aFolderChildrenArray.Length();
  for (uint32_t i = startIndex; i < childCount; ++i) {
    if (aFolderChildrenArray[i].type == TYPE_FOLDER) {
      // nsTarray assumes that all children can be memmove()d, thus we can't
      // just pass aFolderChildrenArray[i].guid to a method that will change
      // the array itself.  Otherwise, since it's passed by reference, after a
      // memmove() it could point to garbage and cause intermittent crashes.
      nsCString guid = aFolderChildrenArray[i].guid;
      GetDescendantChildren(aFolderChildrenArray[i].id, guid, aFolderId,
                            aFolderChildrenArray);
    }
  }

  return NS_OK;
}

nsresult nsNavBookmarks::RemoveFolderChildren(int64_t aFolderId,
                                              uint16_t aSource) {
  AUTO_PROFILER_LABEL("nsNavBookmarks::RemoveFolderChilder", OTHER);
  NS_ENSURE_ARG_MIN(aFolderId, 1);

  BookmarkData folder;
  nsresult rv = FetchItemInfo(aFolderId, folder);
  NS_ENSURE_SUCCESS(rv, rv);
  NS_ENSURE_ARG(folder.type == TYPE_FOLDER);
  NS_ENSURE_ARG(folder.parentId != 0);

  // Fill folder children array recursively.
  nsTArray<BookmarkData> folderChildrenArray;
  rv = GetDescendantChildren(folder.id, folder.guid, folder.parentId,
                             folderChildrenArray);
  NS_ENSURE_SUCCESS(rv, rv);

  // Build a string of folders whose children will be removed.
  nsCString foldersToRemove;
  for (uint32_t i = 0; i < folderChildrenArray.Length(); ++i) {
    BookmarkData& child = folderChildrenArray[i];

    if (child.type == TYPE_FOLDER) {
      foldersToRemove.Append(',');
      foldersToRemove.AppendInt(child.id);
    }
  }

  int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);

  // Delete items from the database now.
  mozStorageTransaction transaction(mDB->MainConn(), false);

  // XXX Handle the error, bug 1696133.
  (void)NS_WARN_IF(NS_FAILED(transaction.Start()));

  nsCOMPtr<mozIStorageStatement> deleteStatement =
      mDB->GetStatement(nsLiteralCString("DELETE FROM moz_bookmarks "
                                         "WHERE parent IN (:parent") +
                        foldersToRemove + ")"_ns);
  NS_ENSURE_STATE(deleteStatement);
  mozStorageStatementScoper deleteStatementScoper(deleteStatement);

  rv = deleteStatement->BindInt64ByName("parent"_ns, folder.id);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = deleteStatement->Execute();
  NS_ENSURE_SUCCESS(rv, rv);

  // Clean up orphan items annotations.
  nsCOMPtr<mozIStorageConnection> conn = mDB->MainConn();
  if (!conn) {
    return NS_ERROR_UNEXPECTED;
  }
  rv = conn->ExecuteSimpleSQL(
      nsLiteralCString("DELETE FROM moz_items_annos "
                       "WHERE id IN ("
                       "SELECT a.id from moz_items_annos a "
                       "LEFT JOIN moz_bookmarks b ON a.item_id = b.id "
                       "WHERE b.id ISNULL)"));
  NS_ENSURE_SUCCESS(rv, rv);

  // Set the lastModified date.
  rv = SetItemDateInternal(LAST_MODIFIED, syncChangeDelta, folder.id,
                           RoundedPRNow());
  NS_ENSURE_SUCCESS(rv, rv);

  int64_t tagsRootId = mDB->GetTagsFolderId();

  if (syncChangeDelta) {
    nsTArray<TombstoneData> tombstones(folderChildrenArray.Length());
    PRTime dateRemoved = RoundedPRNow();

    for (uint32_t i = 0; i < folderChildrenArray.Length(); ++i) {
      BookmarkData& child = folderChildrenArray[i];
      if (NeedsTombstone(child)) {
        // Write tombstones for synced children.
        TombstoneData childTombstone = {child.guid, dateRemoved};
        tombstones.AppendElement(childTombstone);
      }
      bool isUntagging = child.grandParentId == tagsRootId;
      if (isUntagging) {
        // Bump the change counter for all tagged bookmarks when removing a tag
        // folder.
        rv = AddSyncChangesForBookmarksWithURL(child.url, syncChangeDelta);
        NS_ENSURE_SUCCESS(rv, rv);
      }
    }

    rv = InsertTombstones(tombstones);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  rv = transaction.Commit();
  NS_ENSURE_SUCCESS(rv, rv);

  Sequence<OwningNonNull<PlacesEvent>> notifications;
  // Call observers in reverse order to serve children before their parent.
  for (int32_t i = folderChildrenArray.Length() - 1; i >= 0; --i) {
    BookmarkData& child = folderChildrenArray[i];

    nsCOMPtr<nsIURI> uri;
    if (child.type == TYPE_BOOKMARK) {
      // A broken url should not interrupt the removal process.
      (void)NS_NewURI(getter_AddRefs(uri), child.url);
      // We cannot assert since some automated tests are checking this path.
      NS_WARNING_ASSERTION(uri, "Invalid URI in RemoveFolderChildren");
    }

    if (!mCanNotify) {
      return NS_OK;
    }

    RefPtr<PlacesBookmarkRemoved> bookmark = new PlacesBookmarkRemoved();
    bookmark->mItemType = TYPE_BOOKMARK;
    bookmark->mId = child.id;
    bookmark->mParentId = child.parentId;
    bookmark->mIndex = child.position;
    bookmark->mUrl.Assign(NS_ConvertUTF8toUTF16(child.url));
    bookmark->mTitle.Assign(NS_ConvertUTF8toUTF16(child.title));
    bookmark->mGuid.Assign(child.guid);
    bookmark->mParentGuid.Assign(child.parentGuid);
    bookmark->mSource = aSource;
    bookmark->mIsTagging = (child.grandParentId == tagsRootId);
    bookmark->mIsDescendantRemoval = (child.grandParentId != tagsRootId);
    bool success = !!notifications.AppendElement(bookmark.forget(), fallible);
    MOZ_RELEASE_ASSERT(success);

    if (child.type == TYPE_BOOKMARK && child.grandParentId == tagsRootId &&
        uri) {
      // If the removed bookmark was a child of a tag container, notify all
      // bookmark-folder result nodes which contain a bookmark for the removed
      // bookmark's url.
      nsTArray<BookmarkData> bookmarks;
      rv = GetBookmarksForURI(uri, bookmarks);
      NS_ENSURE_SUCCESS(rv, rv);

      nsAutoCString utf8spec;
      uri->GetSpec(utf8spec);

      nsTArray<nsString> tags;
      rv = GetTags(uri, tags);
      NS_ENSURE_SUCCESS(rv, rv);

      for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
        RefPtr<PlacesBookmarkTags> tagsChanged = new PlacesBookmarkTags();
        tagsChanged->mId = bookmarks[i].id;
        tagsChanged->mItemType = TYPE_BOOKMARK;
        tagsChanged->mUrl.Assign(NS_ConvertUTF8toUTF16(utf8spec));
        tagsChanged->mGuid = bookmarks[i].guid;
        tagsChanged->mParentGuid = bookmarks[i].parentGuid;
        tagsChanged->mTags.Assign(tags);
        tagsChanged->mLastModified = bookmarks[i].lastModified / 1000;
        tagsChanged->mSource = aSource;
        tagsChanged->mIsTagging = false;
        success = !!notifications.AppendElement(tagsChanged.forget(), fallible);
        MOZ_RELEASE_ASSERT(success);
      }
    }
  }

  if (notifications.Length()) {
    PlacesObservers::NotifyListeners(notifications);
  }

  return NS_OK;
}

nsresult nsNavBookmarks::FetchItemInfo(int64_t aItemId,
                                       BookmarkData& _bookmark) {
  // LEFT JOIN since not all bookmarks have an associated place.
  nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
      "SELECT b.id, h.url, b.title, b.position, b.fk, b.parent, b.type, "
      "b.dateAdded, b.lastModified, b.guid, t.guid, t.parent, "
      "b.syncStatus "
      "FROM moz_bookmarks b "
      "LEFT JOIN moz_bookmarks t ON t.id = b.parent "
      "LEFT JOIN moz_places h ON h.id = b.fk "
      "WHERE b.id = :item_id");
  NS_ENSURE_STATE(stmt);
  mozStorageStatementScoper scoper(stmt);

  nsresult rv = stmt->BindInt64ByName("item_id"_ns, aItemId);
  NS_ENSURE_SUCCESS(rv, rv);

  bool hasResult;
  rv = stmt->ExecuteStep(&hasResult);
  NS_ENSURE_SUCCESS(rv, rv);
  if (!hasResult) {
    return NS_ERROR_INVALID_ARG;
  }

  _bookmark.id = aItemId;
  rv = stmt->GetUTF8String(1, _bookmark.url);
  NS_ENSURE_SUCCESS(rv, rv);

  bool isNull;
  rv = stmt->GetIsNull(2, &isNull);
  NS_ENSURE_SUCCESS(rv, rv);
  if (!isNull) {
    rv = stmt->GetUTF8String(2, _bookmark.title);
    NS_ENSURE_SUCCESS(rv, rv);
  }
  rv = stmt->GetInt32(3, &_bookmark.position);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = stmt->GetInt64(4, &_bookmark.placeId);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = stmt->GetInt64(5, &_bookmark.parentId);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = stmt->GetInt32(6, &_bookmark.type);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = stmt->GetInt64(7, reinterpret_cast<int64_t*>(&_bookmark.dateAdded));
  NS_ENSURE_SUCCESS(rv, rv);
  rv = stmt->GetInt64(8, reinterpret_cast<int64_t*>(&_bookmark.lastModified));
  NS_ENSURE_SUCCESS(rv, rv);
  rv = stmt->GetUTF8String(9, _bookmark.guid);
  NS_ENSURE_SUCCESS(rv, rv);
  // Getting properties of the root would show no parent.
  rv = stmt->GetIsNull(10, &isNull);
  NS_ENSURE_SUCCESS(rv, rv);
  if (!isNull) {
    rv = stmt->GetUTF8String(10, _bookmark.parentGuid);
    NS_ENSURE_SUCCESS(rv, rv);
    rv = stmt->GetInt64(11, &_bookmark.grandParentId);
    NS_ENSURE_SUCCESS(rv, rv);
  } else {
    _bookmark.grandParentId = -1;
  }
  rv = stmt->GetInt32(12, &_bookmark.syncStatus);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

nsresult nsNavBookmarks::SetItemDateInternal(enum BookmarkDate aDateType,
                                             int64_t aSyncChangeDelta,
                                             int64_t aItemId, PRTime aValue) {
  aValue = RoundToMilliseconds(aValue);

  nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
      "UPDATE moz_bookmarks SET lastModified = :date, "
      "syncChangeCounter = syncChangeCounter + :delta "
      "WHERE id = :item_id");
  NS_ENSURE_STATE(stmt);
  mozStorageStatementScoper scoper(stmt);

  nsresult rv = stmt->BindInt64ByName("date"_ns, aValue);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = stmt->BindInt64ByName("item_id"_ns, aItemId);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = stmt->BindInt64ByName("delta"_ns, aSyncChangeDelta);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = stmt->Execute();
  NS_ENSURE_SUCCESS(rv, rv);

  // note, we are not notifying the observers
  // that the item has changed.

  return NS_OK;
}

NS_IMETHODIMP
nsNavBookmarks::SetItemLastModified(int64_t aItemId, PRTime aLastModified,
                                    uint16_t aSource) {
  NS_ENSURE_ARG_MIN(aItemId, 1);

  BookmarkData bookmark;
  nsresult rv = FetchItemInfo(aItemId, bookmark);
  NS_ENSURE_SUCCESS(rv, rv);

  int64_t tagsRootId = mDB->GetTagsFolderId();
  bool isTagging = bookmark.grandParentId == tagsRootId;
  int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);

  // Round here so that we notify with the right value.
  bookmark.lastModified = RoundToMilliseconds(aLastModified);

  if (isTagging) {
    // If we're changing a tag, bump the change counter for all tagged
    // bookmarks. We use a separate code path to avoid a transaction for
    // non-tags.
    mozStorageTransaction transaction(mDB->MainConn(), false);

    // XXX Handle the error, bug 1696133.
    (void)NS_WARN_IF(NS_FAILED(transaction.Start()));

    rv = SetItemDateInternal(LAST_MODIFIED, syncChangeDelta, bookmark.id,
                             bookmark.lastModified);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = AddSyncChangesForBookmarksWithURL(bookmark.url, syncChangeDelta);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = transaction.Commit();
    NS_ENSURE_SUCCESS(rv, rv);
  } else {
    rv = SetItemDateInternal(LAST_MODIFIED, syncChangeDelta, bookmark.id,
                             bookmark.lastModified);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  // Note: mDBSetItemDateAdded also sets lastModified to aDateAdded.

  if (mCanNotify) {
    Sequence<OwningNonNull<PlacesEvent>> events;
    RefPtr<PlacesBookmarkTime> timeChanged = new PlacesBookmarkTime();
    timeChanged->mId = bookmark.id;
    timeChanged->mItemType = bookmark.type;
    timeChanged->mUrl.Assign(NS_ConvertUTF8toUTF16(bookmark.url));
    timeChanged->mGuid = bookmark.guid;
    timeChanged->mParentGuid = bookmark.parentGuid;
    timeChanged->mDateAdded = bookmark.dateAdded / 1000;
    timeChanged->mLastModified = bookmark.lastModified / 1000;
    timeChanged->mSource = aSource;
    timeChanged->mIsTagging =
        bookmark.parentId == tagsRootId || bookmark.grandParentId == tagsRootId;
    bool success = !!events.AppendElement(timeChanged.forget(), fallible);
    MOZ_RELEASE_ASSERT(success);
    PlacesObservers::NotifyListeners(events);
  }

  return NS_OK;
}

nsresult nsNavBookmarks::AddSyncChangesForBookmarksWithURL(
    const nsACString& aURL, int64_t aSyncChangeDelta) {
  if (!aSyncChangeDelta) {
    return NS_OK;
  }
  nsCOMPtr<nsIURI> uri;
  nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    // Ignore sync changes for invalid URLs.
    return NS_OK;
  }
  return AddSyncChangesForBookmarksWithURI(uri, aSyncChangeDelta);
}

nsresult nsNavBookmarks::AddSyncChangesForBookmarksWithURI(
    nsIURI* aURI, int64_t aSyncChangeDelta) {
  if (NS_WARN_IF(!aURI) || !aSyncChangeDelta) {
    // Ignore sync changes for invalid URIs.
    return NS_OK;
  }

  nsCOMPtr<mozIStorageStatement> statement = mDB->GetStatement(
      "UPDATE moz_bookmarks SET "
      "syncChangeCounter = syncChangeCounter + :delta "
      "WHERE type = :type AND "
      "fk = (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND "
      "url = :url)");
  NS_ENSURE_STATE(statement);
  mozStorageStatementScoper scoper(statement);

  nsresult rv = statement->BindInt64ByName("delta"_ns, aSyncChangeDelta);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = statement->BindInt64ByName("type"_ns,
                                  nsINavBookmarksService::TYPE_BOOKMARK);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = URIBinder::Bind(statement, "url"_ns, aURI);
  NS_ENSURE_SUCCESS(rv, rv);

  return statement->Execute();
}

nsresult nsNavBookmarks::AddSyncChangesForBookmarksInFolder(
    int64_t aFolderId, int64_t aSyncChangeDelta) {
  if (!aSyncChangeDelta) {
    return NS_OK;
  }

  nsCOMPtr<mozIStorageStatement> statement = mDB->GetStatement(
      "UPDATE moz_bookmarks SET "
      "syncChangeCounter = syncChangeCounter + :delta "
      "WHERE type = :type AND "
      "fk = (SELECT fk FROM moz_bookmarks WHERE parent = :parent)");
  NS_ENSURE_STATE(statement);
  mozStorageStatementScoper scoper(statement);

  nsresult rv = statement->BindInt64ByName("delta"_ns, aSyncChangeDelta);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = statement->BindInt64ByName("type"_ns,
                                  nsINavBookmarksService::TYPE_BOOKMARK);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = statement->BindInt64ByName("parent"_ns, aFolderId);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = statement->Execute();
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

nsresult nsNavBookmarks::InsertTombstone(const BookmarkData& aBookmark) {
  if (!NeedsTombstone(aBookmark)) {
    return NS_OK;
  }
  nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
      "INSERT INTO moz_bookmarks_deleted (guid, dateRemoved) "
      "VALUES (:guid, :date_removed)");
  NS_ENSURE_STATE(stmt);
  mozStorageStatementScoper scoper(stmt);

  nsresult rv = stmt->BindUTF8StringByName("guid"_ns, aBookmark.guid);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = stmt->BindInt64ByName("date_removed"_ns, RoundedPRNow());
  NS_ENSURE_SUCCESS(rv, rv);

  rv = stmt->Execute();
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

nsresult nsNavBookmarks::InsertTombstones(
    const nsTArray<TombstoneData>& aTombstones) {
  if (aTombstones.IsEmpty()) {
    return NS_OK;
  }

  nsCOMPtr<mozIStorageConnection> conn = mDB->MainConn();
  NS_ENSURE_STATE(conn);

  int32_t variableLimit = 0;
  nsresult rv = conn->GetVariableLimit(&variableLimit);
  NS_ENSURE_SUCCESS(rv, rv);

  size_t maxRowsPerChunk = variableLimit / 2;
  for (uint32_t startIndex = 0; startIndex < aTombstones.Length();
       startIndex += maxRowsPerChunk) {
    size_t rowsPerChunk =
        std::min(maxRowsPerChunk, aTombstones.Length() - startIndex);

    // Build a query to insert all tombstones in a single statement, chunking to
    // avoid the SQLite bound parameter limit.
    nsAutoCString tombstonesToInsert;
    tombstonesToInsert.AppendLiteral("VALUES (?, ?)");
    for (uint32_t i = 1; i < rowsPerChunk; ++i) {
      tombstonesToInsert.AppendLiteral(", (?, ?)");
    }
#ifdef DEBUG
    MOZ_ASSERT(tombstonesToInsert.CountChar('?') == rowsPerChunk * 2,
               "Expected one binding param per column for each tombstone");
#endif

    nsCOMPtr<mozIStorageStatement> stmt =
        mDB->GetStatement(nsLiteralCString("INSERT INTO moz_bookmarks_deleted "
                                           "(guid, dateRemoved) ") +
                          tombstonesToInsert);
    NS_ENSURE_STATE(stmt);
    mozStorageStatementScoper scoper(stmt);

    uint32_t paramIndex = 0;
    for (uint32_t i = 0; i < rowsPerChunk; ++i) {
      const TombstoneData& tombstone = aTombstones[startIndex + i];
      rv = stmt->BindUTF8StringByIndex(paramIndex++, tombstone.guid);
      NS_ENSURE_SUCCESS(rv, rv);
      rv = stmt->BindInt64ByIndex(paramIndex++, tombstone.dateRemoved);
      NS_ENSURE_SUCCESS(rv, rv);
    }

    rv = stmt->Execute();
    NS_ENSURE_SUCCESS(rv, rv);
  }

  return NS_OK;
}

nsresult nsNavBookmarks::RemoveTombstone(const nsACString& aGUID) {
  nsCOMPtr<mozIStorageStatement> stmt =
      mDB->GetStatement("DELETE FROM moz_bookmarks_deleted WHERE guid = :guid");
  NS_ENSURE_STATE(stmt);
  mozStorageStatementScoper scoper(stmt);

  nsresult rv = stmt->BindUTF8StringByName("guid"_ns, aGUID);
  NS_ENSURE_SUCCESS(rv, rv);

  return stmt->Execute();
}

NS_IMETHODIMP
nsNavBookmarks::SetItemTitle(int64_t aItemId, const nsACString& aTitle,
                             uint16_t aSource) {
  NS_ENSURE_ARG_MIN(aItemId, 1);

  BookmarkData bookmark;
  nsresult rv = FetchItemInfo(aItemId, bookmark);
  NS_ENSURE_SUCCESS(rv, rv);

  int64_t tagsRootId = mDB->GetTagsFolderId();
  bool isChangingTagFolder = bookmark.parentId == tagsRootId;
  int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);

  nsAutoCString title;
  TruncateTitle(aTitle, title);

  if (isChangingTagFolder) {
    // If we're changing the title of a tag folder, bump the change counter
    // for all tagged bookmarks. We use a separate code path to avoid a
    // transaction for non-tags.
    mozStorageTransaction transaction(mDB->MainConn(), false);

    // XXX Handle the error, bug 1696133.
    (void)NS_WARN_IF(NS_FAILED(transaction.Start()));

    rv = SetItemTitleInternal(bookmark, title, syncChangeDelta);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = AddSyncChangesForBookmarksInFolder(bookmark.id, syncChangeDelta);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = transaction.Commit();
    NS_ENSURE_SUCCESS(rv, rv);
  } else {
    rv = SetItemTitleInternal(bookmark, title, syncChangeDelta);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  if (mCanNotify) {
    Sequence<OwningNonNull<PlacesEvent>> events;
    RefPtr<PlacesBookmarkTitle> titleChanged = new PlacesBookmarkTitle();
    titleChanged->mId = bookmark.id;
    titleChanged->mItemType = bookmark.type;
    titleChanged->mUrl.Assign(NS_ConvertUTF8toUTF16(bookmark.url));
    titleChanged->mGuid = bookmark.guid;
    titleChanged->mParentGuid = bookmark.parentGuid;
    titleChanged->mTitle.Assign(NS_ConvertUTF8toUTF16(title));
    titleChanged->mLastModified = bookmark.lastModified / 1000;
    titleChanged->mSource = aSource;
    titleChanged->mIsTagging =
        bookmark.parentId == tagsRootId || bookmark.grandParentId == tagsRootId;
    bool success = !!events.AppendElement(titleChanged.forget(), fallible);
    MOZ_RELEASE_ASSERT(success);
    PlacesObservers::NotifyListeners(events);
  }

  return NS_OK;
}

nsresult nsNavBookmarks::SetItemTitleInternal(BookmarkData& aBookmark,
                                              const nsACString& aTitle,
                                              int64_t aSyncChangeDelta) {
  nsCOMPtr<mozIStorageStatement> statement = mDB->GetStatement(
      "UPDATE moz_bookmarks SET "
      "title = :item_title, lastModified = :date, "
      "syncChangeCounter = syncChangeCounter + :delta "
      "WHERE id = :item_id");
  NS_ENSURE_STATE(statement);
  mozStorageStatementScoper scoper(statement);

  nsresult rv;
  if (aTitle.IsEmpty()) {
    rv = statement->BindNullByName("item_title"_ns);
  } else {
    rv = statement->BindUTF8StringByName("item_title"_ns, aTitle);
  }
  NS_ENSURE_SUCCESS(rv, rv);
  aBookmark.lastModified = RoundToMilliseconds(RoundedPRNow());
  rv = statement->BindInt64ByName("date"_ns, aBookmark.lastModified);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = statement->BindInt64ByName("item_id"_ns, aBookmark.id);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = statement->BindInt64ByName("delta"_ns, aSyncChangeDelta);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = statement->Execute();
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

NS_IMETHODIMP
nsNavBookmarks::GetItemTitle(int64_t aItemId, nsACString& _title) {
  NS_ENSURE_ARG_MIN(aItemId, 1);

  BookmarkData bookmark;
  nsresult rv = FetchItemInfo(aItemId, bookmark);
  NS_ENSURE_SUCCESS(rv, rv);

  _title = bookmark.title;
  return NS_OK;
}

nsresult nsNavBookmarks::FetchFolderInfo(int64_t aFolderId,
                                         int32_t* _folderCount,
                                         nsACString& _guid,
                                         int64_t* _parentId) {
  *_folderCount = 0;
  *_parentId = -1;

  // This query has to always return results, so it can't be written as a join,
  // though a left join of 2 subqueries would have the same cost.
  nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
      "SELECT count(*), "
      "(SELECT guid FROM moz_bookmarks WHERE id = :parent), "
      "(SELECT parent FROM moz_bookmarks WHERE id = :parent) "
      "FROM moz_bookmarks "
      "WHERE parent = :parent");
  NS_ENSURE_STATE(stmt);
  mozStorageStatementScoper scoper(stmt);

  nsresult rv = stmt->BindInt64ByName("parent"_ns, aFolderId);
  NS_ENSURE_SUCCESS(rv, rv);

  bool hasResult;
  rv = stmt->ExecuteStep(&hasResult);
  NS_ENSURE_SUCCESS(rv, rv);
  NS_ENSURE_TRUE(hasResult, NS_ERROR_UNEXPECTED);

  // Ensure that the folder we are looking for exists.
  // Can't rely only on parent, since the root has parent 0, that doesn't exist.
  bool isNull;
  rv = stmt->GetIsNull(2, &isNull);
  NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && (!isNull || aFolderId == 0),
                 NS_ERROR_INVALID_ARG);

  rv = stmt->GetInt32(0, _folderCount);
  NS_ENSURE_SUCCESS(rv, rv);
  if (!isNull) {
    rv = stmt->GetUTF8String(1, _guid);
    NS_ENSURE_SUCCESS(rv, rv);
    rv = stmt->GetInt64(2, _parentId);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  return NS_OK;
}

nsresult nsNavBookmarks::GetBookmarksForURI(
    nsIURI* aURI, nsTArray<BookmarkData>& aBookmarks) {
  NS_ENSURE_ARG(aURI);

  // Double ordering covers possible lastModified ties, that could happen when
  // importing, syncing or due to extensions.
  // Note: not using a JOIN is cheaper in this case.
  nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
      "/* do not warn (bug 1175249) */ "
      "SELECT b.id, b.guid, b.parent, b.lastModified, t.guid, t.parent, "
      "b.syncStatus "
      "FROM moz_bookmarks b "
      "JOIN moz_bookmarks t on t.id = b.parent "
      "WHERE b.fk = (SELECT id FROM moz_places WHERE url_hash = "
      "hash(:page_url) AND url = :page_url) "
      "ORDER BY b.lastModified DESC, b.id DESC ");
  NS_ENSURE_STATE(stmt);
  mozStorageStatementScoper scoper(stmt);

  nsresult rv = URIBinder::Bind(stmt, "page_url"_ns, aURI);
  NS_ENSURE_SUCCESS(rv, rv);

  int64_t tagsRootId = mDB->GetTagsFolderId();

  bool more;
  nsAutoString tags;
  while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&more))) && more) {
    // Skip tags.
    int64_t grandParentId;
    nsresult rv = stmt->GetInt64(5, &grandParentId);
    NS_ENSURE_SUCCESS(rv, rv);
    if (grandParentId == tagsRootId) {
      continue;
    }

    BookmarkData bookmark;
    bookmark.grandParentId = grandParentId;
    rv = stmt->GetInt64(0, &bookmark.id);
    NS_ENSURE_SUCCESS(rv, rv);
    rv = stmt->GetUTF8String(1, bookmark.guid);
    NS_ENSURE_SUCCESS(rv, rv);
    rv = stmt->GetInt64(2, &bookmark.parentId);
    NS_ENSURE_SUCCESS(rv, rv);
    rv = stmt->GetInt64(3, reinterpret_cast<int64_t*>(&bookmark.lastModified));
    NS_ENSURE_SUCCESS(rv, rv);
    rv = stmt->GetUTF8String(4, bookmark.parentGuid);
    NS_ENSURE_SUCCESS(rv, rv);
    rv = stmt->GetInt32(6, &bookmark.syncStatus);
    NS_ENSURE_SUCCESS(rv, rv);

    aBookmarks.AppendElement(bookmark);
  }

  return NS_OK;
}

////////////////////////////////////////////////////////////////////////////////
//// nsIObserver

NS_IMETHODIMP
nsNavBookmarks::Observe(nsISupports* aSubject, const char* aTopic,
                        const char16_t* aData) {
  NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");

  if (strcmp(aTopic, TOPIC_PLACES_CONNECTION_CLOSED) == 0) {
    // Don't even try to notify observers from this point on, the category
    // cache would init services that could try to use our APIs.
    mCanNotify = false;
  }

  return NS_OK;
}
