/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/. */

Components.utils.import("resource://gre/modules/Services.jsm");

const LAST_USED_ANNO = "bookmarkPropertiesDialog/folderLastUsed";
const MAX_FOLDER_ITEM_IN_MENU_LIST = 5;

var gEditItemOverlay = {
  _uri: null,
  _itemId: -1,
  _itemIds: [],
  _uris: [],
  _tags: [],
  _allTags: [],
  _multiEdit: false,
  _itemType: -1,
  _readOnly: false,
  _hiddenRows: [],
  _observersAdded: false,
  _staticFoldersListBuilt: false,
  _initialized: false,
  _titleOverride: null,

  // the first field which was edited after this panel was initialized for
  // a certain item
  _firstEditedField: "",

  get itemId() {
    return this._itemId;
  },

  get uri() {
    return this._uri;
  },

  get multiEdit() {
    return this._multiEdit;
  },

  /**
   * Determines the initial data for the item edited or added by this dialog
   */
  _determineInfo: function EIO__determineInfo(aInfo) {
    // hidden rows
    if (aInfo && aInfo.hiddenRows)
      this._hiddenRows = aInfo.hiddenRows;
    else
      this._hiddenRows.splice(0, this._hiddenRows.length);
    // force-read-only
    this._readOnly = aInfo && aInfo.forceReadOnly;
    // override title
    this._titleOverride = aInfo && aInfo.titleOverride;
  },

  _showHideRows: function EIO__showHideRows() {
    var isBookmark = this._itemId != -1 &&
                     this._itemType == Components.interfaces.nsINavBookmarksService.TYPE_BOOKMARK;
    var isQuery = false;
    if (this._uri)
      isQuery = this._uri.schemeIs("place");

    this._element("nameRow").collapsed = this._hiddenRows.indexOf("name") != -1;
    this._element("folderRow").collapsed =
      this._hiddenRows.indexOf("folderPicker") != -1 || this._readOnly;
    this._element("tagsRow").collapsed = !this._uri ||
      this._hiddenRows.indexOf("tags") != -1 || isQuery;
    // Collapse the tag selector if the item does not accept tags.
    if (!this._element("tagsSelectorRow").collapsed &&
        this._element("tagsRow").collapsed)
      this.toggleTagsSelector();
    this._element("descriptionRow").collapsed =
      this._hiddenRows.indexOf("description") != -1 || this._readOnly;
    this._element("keywordRow").collapsed = !isBookmark || this._readOnly ||
      this._hiddenRows.indexOf("keyword") != -1 || isQuery;
    this._element("locationRow").collapsed = !(this._uri && !isQuery) ||
      this._hiddenRows.indexOf("location") != -1;
    this._element("feedLocationRow").collapsed = !this._isLivemark ||
      this._hiddenRows.indexOf("feedLocation") != -1;
    this._element("siteLocationRow").collapsed = !this._isLivemark ||
      this._hiddenRows.indexOf("siteLocation") != -1;
    this._element("selectionCount").hidden = !this._multiEdit;
  },

  /**
   * Initialize the panel
   * @param aFor
   *        Either a places-itemId (of a bookmark, folder or a live bookmark),
   *        an array of itemIds (used for bulk tagging), or a URI object (in
   *        which case, the panel would be initialized in read-only mode).
   * @param [optional] aInfo
   *        JS object which stores additional info for the panel
   *        initialization. The following properties may bet set:
   *        * hiddenRows (Strings array): list of rows to be hidden regardless
   *          of the item edited. Possible values: "title", "location",
   *          "description", "keyword", "feedLocation",
   *          "siteLocation", folderPicker"
   *        * forceReadOnly - set this flag to initialize the panel to its
   *          read-only (view) mode even if the given item is editable.
   */
  initPanel: function EIO_initPanel(aFor, aInfo) {
    // For sanity ensure that the implementer has uninited the panel before
    // trying to init it again, or we could end up leaking due to observers.
    if (this._initialized)
      this.uninitPanel(false);

    var aItemIdList;
    if (Array.isArray(aFor)) {
      aItemIdList = aFor;
      aFor = aItemIdList[0];
    }
    else if (this._multiEdit) {
      this._multiEdit = false;
      this._tags = [];
      this._uris = [];
      this._allTags = [];
      this._itemIds = [];
      this._element("selectionCount").hidden = true;
    }

    this._folderMenuList = this._element("folderMenuList");
    this._folderTree = this._element("folderTree");

    this._determineInfo(aInfo);
    if (aFor instanceof Components.interfaces.nsIURI) {
      this._itemId = -1;
      this._uri = aFor;
      this._readOnly = true;
    }
    else {
      this._itemId = aFor;
      // We can't store information on livemarks.
      if (aFor == -1)
        this._readOnly = true;
      var containerId = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId);
      this._itemType = PlacesUtils.bookmarks.getItemType(this._itemId);
      if (this._itemType == Components.interfaces.nsINavBookmarksService.TYPE_BOOKMARK) {
        this._uri = PlacesUtils.bookmarks.getBookmarkURI(this._itemId);
        this._initTextField("keywordField",
                            PlacesUtils.bookmarks
                                       .getKeywordForBookmark(this._itemId));
      }
      else {
        this._uri = null;
        this._isLivemark = false;
        PlacesUtils.livemarks.getLivemark({ id: this._itemId })
                             .then(aLivemark => {
          this._isLivemark = true;
          this._initTextField("feedLocationField", aLivemark.feedURI.spec, true);
          this._initTextField("siteLocationField",
                              aLivemark.siteURI ? aLivemark.siteURI.spec : "", true);
          this._showHideRows();
        }, () => undefined);
      }

      // folder picker
      this._initFolderMenuList(containerId);

      // description field
      this._initTextField("descriptionField",
                          PlacesUIUtils.getItemDescription(this._itemId));
    }

    if (this._itemId == -1 ||
        this._itemType == Components.interfaces.nsINavBookmarksService.TYPE_BOOKMARK) {
      this._isLivemark = false;

      this._initTextField("locationField", this._uri.spec);
      if (!aItemIdList) {
        var tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", ");
        this._initTextField("tagsField", tags, false);
      }
      else {
        this._multiEdit = true;
        this._allTags = [];
        this._itemIds = aItemIdList;
        for (var i = 0; i < aItemIdList.length; i++) {
          if (aItemIdList[i] instanceof Components.interfaces.nsIURI) {
            this._uris[i] = aItemIdList[i];
            this._itemIds[i] = -1;
          }
          else
            this._uris[i] = PlacesUtils.bookmarks.getBookmarkURI(this._itemIds[i]);
          this._tags[i] = PlacesUtils.tagging.getTagsForURI(this._uris[i]);
        }
        this._allTags = this._getCommonTags();
        this._initTextField("tagsField", this._allTags.join(", "), false);
        this._element("itemsCountText").value =
          PlacesUIUtils.getFormattedString("detailsPane.multipleItems",
                                           [this._itemIds.length]);
      }

      // tags selector
      this._rebuildTagsSelectorList();
    }

    // name picker
    this._initNamePicker();

    this._showHideRows();

    // observe changes
    if (!this._observersAdded) {
      // Single bookmarks observe any change.  History entries and multiEdit
      // observe only tags changes, through bookmarks.
      if (this._itemId != -1 || this._uri || this._multiEdit)
        PlacesUtils.bookmarks.addObserver(this, false);
      window.addEventListener("unload", this, false);
      this._observersAdded = true;
    }

    this._initialized = true;
  },

  /**
   * Finds tags that are in common among this._tags entries that track tags
   * for each selected uri.
   * The tags arrays should be kept up-to-date for this to work properly.
   *
   * @return array of common tags for the selected uris.
   */
  _getCommonTags: function() {
    return this._tags[0].filter(
      aTag => this._tags.every(
        aTags => aTags.indexOf(aTag) != -1
      )
    );
  },

  _initTextField: function(aTextFieldId, aValue, aReadOnly) {
    var field = this._element(aTextFieldId);
    field.readOnly = aReadOnly !== undefined ? aReadOnly : this._readOnly;

    if (field.value != aValue) {
      field.value = aValue;

      // clear the undo stack
      var editor = field.editor;
      if (editor)
        editor.transactionManager.clear();
    }
  },

  /**
   * Appends a menu-item representing a bookmarks folder to a menu-popup.
   * @param aMenupopup
   *        The popup to which the menu-item should be added.
   * @param aFolderId
   *        The identifier of the bookmarks folder.
   * @return the new menu item.
   */
  _appendFolderItemToMenupopup:
  function EIO__appendFolderItemToMenuList(aMenupopup, aFolderId) {
    // First make sure the folders-separator is visible
    this._element("foldersSeparator").hidden = false;

    var folderMenuItem = document.createElement("menuitem");
    var folderTitle = PlacesUtils.bookmarks.getItemTitle(aFolderId)
    folderMenuItem.folderId = aFolderId;
    folderMenuItem.setAttribute("label", folderTitle);
    folderMenuItem.className = "menuitem-iconic folder-icon";
    aMenupopup.appendChild(folderMenuItem);
    return folderMenuItem;
  },

  _initFolderMenuList: function EIO__initFolderMenuList(aSelectedFolder) {
    // clean up first
    var menupopup = this._folderMenuList.menupopup;
    while (menupopup.childNodes.length > 6)
      menupopup.lastChild.remove();

    const bms = PlacesUtils.bookmarks;
    const annos = PlacesUtils.annotations;

    // Build the static list
    var unfiledItem = this._element("unfiledRootItem");
    if (!this._staticFoldersListBuilt) {
      unfiledItem.label = bms.getItemTitle(PlacesUtils.unfiledBookmarksFolderId);
      unfiledItem.folderId = PlacesUtils.unfiledBookmarksFolderId;
      var bmMenuItem = this._element("bmRootItem");
      bmMenuItem.label = bms.getItemTitle(PlacesUtils.bookmarksMenuFolderId);
      bmMenuItem.folderId = PlacesUtils.bookmarksMenuFolderId;
      var toolbarItem = this._element("toolbarFolderItem");
      toolbarItem.label = bms.getItemTitle(PlacesUtils.toolbarFolderId);
      toolbarItem.folderId = PlacesUtils.toolbarFolderId;
      this._staticFoldersListBuilt = true;
    }

    // List of recently used folders:
    var folderIds = annos.getItemsWithAnnotation(LAST_USED_ANNO);

    /**
     * The value of the LAST_USED_ANNO annotation is the time (in the form of
     * Date.getTime) at which the folder has been last used.
     *
     * First we build the annotated folders array, each item has both the
     * folder identifier and the time at which it was last-used by this dialog
     * set. Then we sort it descendingly based on the time field.
     */
    this._recentFolders = [];
    for (var i = 0; i < folderIds.length; i++) {
      var lastUsed = annos.getItemAnnotation(folderIds[i], LAST_USED_ANNO);
      this._recentFolders.push({ folderId: folderIds[i], lastUsed: lastUsed });
    }
    this._recentFolders.sort(function(a, b) {
      if (b.lastUsed < a.lastUsed)
        return -1;
      if (b.lastUsed > a.lastUsed)
        return 1;
      return 0;
    });

    var numberOfItems = Math.min(MAX_FOLDER_ITEM_IN_MENU_LIST,
                                 this._recentFolders.length);
    for (var i = 0; i < numberOfItems; i++) {
      this._appendFolderItemToMenupopup(menupopup,
                                        this._recentFolders[i].folderId);
    }

    var defaultItem = this._getFolderMenuItem(aSelectedFolder);
    this._folderMenuList.selectedItem = defaultItem;

    // Set a selectedIndex attribute to show special icons
    this._folderMenuList.setAttribute("selectedIndex",
                                      this._folderMenuList.selectedIndex);

    // Hide the folders-separator if no folder is annotated as recently-used
    this._element("foldersSeparator").hidden = (menupopup.childNodes.length <= 6);
    this._folderMenuList.disabled = this._readOnly;
  },

  QueryInterface: function EIO_QueryInterface(aIID) {
    if (aIID.equals(Components.interfaces.nsIDOMEventListener) ||
        aIID.equals(Components.interfaces.nsINavBookmarkObserver) ||
        aIID.equals(Components.interfaces.nsISupports))
      return this;

    throw Components.results.NS_ERROR_NO_INTERFACE;
  },

  _element: function EIO__element(aID) {
    return document.getElementById("editBMPanel_" + aID);
  },

  _getItemStaticTitle: function EIO__getItemStaticTitle() {
    if (this._titleOverride)
      return this._titleOverride;

    if (this._itemId == -1)
      return PlacesUtils.history.getPageTitle(this._uri);

    return PlacesUtils.bookmarks.getItemTitle(this._itemId);
  },

  _initNamePicker: function EIO_initNamePicker() {
    var namePicker = this._element("namePicker");

    namePicker.value = this._getItemStaticTitle();
    namePicker.readOnly = this._readOnly;

    // clear the undo stack
    var editor = namePicker.editor;
    if (editor)
      editor.transactionManager.clear();
  },

  uninitPanel: function EIO_uninitPanel(aHideCollapsibleElements) {
    if (aHideCollapsibleElements) {
      // hide the folder tree if it was previously visible
      var folderTreeRow = this._element("folderTreeRow");
      if (!folderTreeRow.collapsed)
        this.toggleFolderTreeVisibility();

      // hide the tag selector if it was previously visible
      var tagsSelectorRow = this._element("tagsSelectorRow");
      if (!tagsSelectorRow.collapsed)
        this.toggleTagsSelector();
    }

    if (this._observersAdded) {
      if (this._itemId != -1 || this._uri || this._multiEdit)
        PlacesUtils.bookmarks.removeObserver(this);

      this._observersAdded = false;
    }

    this._itemId = -1;
    this._uri = null;
    this._uris = [];
    this._tags = [];
    this._allTags = [];
    this._itemIds = [];
    this._multiEdit = false;
    this._firstEditedField = "";
    this._initialized = false;
    this._readOnly = false;
    this._titleOverride = null;
  },

  onTagsFieldBlur: function EIO_onTagsFieldBlur() {
    if (this._updateTags()) // if anything has changed
      this._mayUpdateFirstEditField("tagsField");
  },

  _updateTags: function EIO__updateTags() {
    if (this._multiEdit)
      return this._updateMultipleTagsForItems();
    return this._updateSingleTagForItem();
  },

  _updateSingleTagForItem: function EIO__updateSingleTagForItem() {
    var currentTags = PlacesUtils.tagging.getTagsForURI(this._uri);
    var tags = this._getTagsArrayFromTagField();
    if (tags.length > 0 || currentTags.length > 0) {
      var tagsToRemove = [];
      var tagsToAdd = [];
      var txns = [];
      for (var i = 0; i < currentTags.length; i++) {
        if (tags.indexOf(currentTags[i]) == -1)
          tagsToRemove.push(currentTags[i]);
      }
      for (var i = 0; i < tags.length; i++) {
        if (currentTags.indexOf(tags[i]) == -1)
          tagsToAdd.push(tags[i]);
      }

      if (tagsToRemove.length > 0) {
        let untagTxn = new PlacesUntagURITransaction(this._uri, tagsToRemove);
        txns.push(untagTxn);
      }
      if (tagsToAdd.length > 0) {
        let tagTxn = new PlacesTagURITransaction(this._uri, tagsToAdd);
        txns.push(tagTxn);
      }

      if (txns.length > 0) {
        let aggregate = new PlacesAggregatedTransaction("Update tags", txns);
        PlacesUtils.transactionManager.doTransaction(aggregate);

        // Ensure the tagsField is in sync, clean it up from empty tags
        var tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", ");
        this._initTextField("tagsField", tags, false);
        return true;
      }
    }
    return false;
  },

   /**
    * Stores the first-edit field for this dialog, if the passed-in field
    * is indeed the first edited field
    * @param aNewField
    *        the id of the field that may be set (without the "editBMPanel_"
    *        prefix)
    */
  _mayUpdateFirstEditField: function EIO__mayUpdateFirstEditField(aNewField) {
    // * The first-edit-field behavior is not applied in the multi-edit case
    // * if this._firstEditedField is already set, this is not the first field,
    //   so there's nothing to do
    if (this._multiEdit || this._firstEditedField)
      return;

    this._firstEditedField = aNewField;

    // set the pref
    Services.prefs.setCharPref("browser.bookmarks.editDialog.firstEditField", aNewField);
  },

  _updateMultipleTagsForItems: function EIO__updateMultipleTagsForItems() {
    var tags = this._getTagsArrayFromTagField();
    if (tags.length > 0 || this._allTags.length > 0) {
      var tagsToRemove = [];
      var tagsToAdd = [];
      var txns = [];
      for (var i = 0; i < this._allTags.length; i++) {
        if (tags.indexOf(this._allTags[i]) == -1)
          tagsToRemove.push(this._allTags[i]);
      }
      for (var i = 0; i < this._tags.length; i++) {
        tagsToAdd[i] = [];
        for (var j = 0; j < tags.length; j++) {
          if (this._tags[i].indexOf(tags[j]) == -1)
            tagsToAdd[i].push(tags[j]);
        }
      }

      if (tagsToAdd.length > 0) {
        for (let i = 0; i < this._uris.length; i++) {
          if (tagsToAdd[i].length > 0) {
            let tagTxn = new PlacesTagURITransaction(this._uris[i],
                                                     tagsToAdd[i]);
            txns.push(tagTxn);
          }
        }
      }
      if (tagsToRemove.length > 0) {
        for (let i = 0; i < this._uris.length; i++) {
          let untagTxn = new PlacesUntagURITransaction(this._uris[i],
                                                       tagsToRemove);
          txns.push(untagTxn);
        }
      }

      if (txns.length > 0) {
        let aggregate = new PlacesAggregatedTransaction("Update tags", txns);
        PlacesUtils.transactionManager.doTransaction(aggregate);

        this._allTags = tags;
        this._tags = [];
        for (let i = 0; i < this._uris.length; i++)
          this._tags[i] = PlacesUtils.tagging.getTagsForURI(this._uris[i]);

        // Ensure the tagsField is in sync, clean it up from empty tags
        this._initTextField("tagsField", tags, false);
        return true;
      }
    }
    return false;
  },

  onNamePickerChange: function EIO_onNamePickerChange() {
    if (this._itemId == -1)
      return;

    var namePicker = this._element("namePicker")

    // Here we update either the item title or its cached static title
    var newTitle = namePicker.value;
    if (!newTitle &&
        PlacesUtils.bookmarks.getFolderIdForItem(this._itemId) == PlacesUtils.tagsFolderId) {
      // We don't allow setting an empty title for a tag, restore the old one.
      this._initNamePicker();
    }
    else if (this._getItemStaticTitle() != newTitle) {
      this._mayUpdateFirstEditField("namePicker");
      let txn = new PlacesEditItemTitleTransaction(this._itemId, newTitle);
      PlacesUtils.transactionManager.doTransaction(txn);
    }
  },

  onDescriptionFieldBlur: function EIO_onDescriptionFieldBlur() {
    var description = this._element("descriptionField").value;
    if (description != PlacesUIUtils.getItemDescription(this._itemId)) {
      const nsIAnnotationService = Components.interfaces.nsIAnnotationService;
      var annoObj = { name   : PlacesUIUtils.DESCRIPTION_ANNO,
                      type   : nsIAnnotationService.TYPE_STRING,
                      flags  : 0,
                      value  : description,
                      expires: nsIAnnotationService.EXPIRE_NEVER };
      var txn = new PlacesSetItemAnnotationTransaction(this._itemId, annoObj);
      PlacesUtils.transactionManager.doTransaction(txn);
    }
  },

  onLocationFieldBlur: function EIO_onLocationFieldBlur() {
    var uri;
    try {
      uri = PlacesUIUtils.createFixedURI(this._element("locationField").value);
    }
    catch(ex) { return; }

    if (!this._uri.equals(uri)) {
      var txn = new PlacesEditBookmarkURITransaction(this._itemId, uri);
      PlacesUtils.transactionManager.doTransaction(txn);
      this._uri = uri;
    }
  },

  onKeywordFieldBlur: function EIO_onKeywordFieldBlur() {
    var keyword = this._element("keywordField").value;
    if (keyword != PlacesUtils.bookmarks.getKeywordForBookmark(this._itemId)) {
      var txn = new PlacesEditBookmarkKeywordTransaction(this._itemId, keyword);
      PlacesUtils.transactionManager.doTransaction(txn);
    }
  },

  toggleFolderTreeVisibility: function EIO_toggleFolderTreeVisibility() {
    var expander = this._element("foldersExpander");
    var folderTreeRow = this._element("folderTreeRow");
    if (!folderTreeRow.collapsed) {
      expander.className = "expander-down";
      expander.setAttribute("tooltiptext",
                            expander.getAttribute("tooltiptextdown"));
      folderTreeRow.collapsed = true;
      this._element("chooseFolderSeparator").hidden =
        this._element("chooseFolderMenuItem").hidden = false;
    }
    else {
      expander.className = "expander-up"
      expander.setAttribute("tooltiptext",
                            expander.getAttribute("tooltiptextup"));
      folderTreeRow.collapsed = false;
      // XXXmano: Ideally we would only do this once, but for some odd reason,
      // the editable mode set on this tree, together with its collapsed state
      // breaks the view.
      const FOLDER_TREE_PLACE_URI =
        "place:excludeItems=1&excludeQueries=1&excludeReadOnlyFolders=1&folder=" +
        PlacesUIUtils.allBookmarksFolderId;
      this._folderTree.place = FOLDER_TREE_PLACE_URI;

      this._element("chooseFolderSeparator").hidden =
        this._element("chooseFolderMenuItem").hidden = true;
      var currentFolder = this._getFolderIdFromMenuList();
      this._folderTree.selectItems([currentFolder]);
      this._folderTree.focus();
    }
  },

  _getFolderIdFromMenuList:
  function EIO__getFolderIdFromMenuList() {
    var selectedItem = this._folderMenuList.selectedItem;
    NS_ASSERT("folderId" in selectedItem,
              "Invalid menuitem in the folders-menulist");
    return selectedItem.folderId;
  },

  /**
   * Get the corresponding menu-item in the folder-menu-list for a bookmarks
   * folder if such an item exists. Otherwise, this creates a menu-item for the
   * folder. If the items-count limit (see MAX_FOLDERS_IN_MENU_LIST) is reached,
   * the new item replaces the last menu-item.
   * @param aFolderId
   *        The identifier of the bookmarks folder.
   */
  _getFolderMenuItem:
  function EIO__getFolderMenuItem(aFolderId) {
    var menupopup = this._folderMenuList.menupopup;

    for (let i = 0; i < menupopup.childNodes.length; i++) {
      if ("folderId" in menupopup.childNodes[i] &&
          menupopup.childNodes[i].folderId == aFolderId)
        return menupopup.childNodes[i];
    }

    // 3 special folders + separator + folder-items-count limit
    if (menupopup.childNodes.length == 4 + MAX_FOLDER_ITEM_IN_MENU_LIST)
      menupopup.lastChild.remove();

    return this._appendFolderItemToMenupopup(menupopup, aFolderId);
  },

  onFolderMenuListCommand: function EIO_onFolderMenuListCommand(aEvent) {
    // Set a selectedIndex attribute to show special icons
    this._folderMenuList.setAttribute("selectedIndex",
                                      this._folderMenuList.selectedIndex);

    if (aEvent.target.id == "editBMPanel_chooseFolderMenuItem") {
      // reset the selection back to where it was and expand the tree
      // (this menu-item is hidden when the tree is already visible
      var container = PlacesUtils.bookmarks.getFolderIdForItem(this._itemId);
      var item = this._getFolderMenuItem(container);
      this._folderMenuList.selectedItem = item;
      // XXXmano HACK: setTimeout 100, otherwise focus goes back to the
      // menulist right away
      setTimeout(() => this.toggleFolderTreeVisibility(), 100);
      return;
    }

    // Move the item
    var container = this._getFolderIdFromMenuList();
    if (PlacesUtils.bookmarks.getFolderIdForItem(this._itemId) != container) {
      var txn = new PlacesMoveItemTransaction(this._itemId,
                                              container,
                                              PlacesUtils.bookmarks.DEFAULT_INDEX);
      PlacesUtils.transactionManager.doTransaction(txn);

      // Mark the containing folder as recently-used if it isn't in the
      // static list
      if (container != PlacesUtils.unfiledBookmarksFolderId &&
          container != PlacesUtils.toolbarFolderId &&
          container != PlacesUtils.bookmarksMenuFolderId)
        this._markFolderAsRecentlyUsed(container);
    }

    // Update folder-tree selection
    var folderTreeRow = this._element("folderTreeRow");
    if (!folderTreeRow.collapsed) {
      var selectedNode = this._folderTree.selectedNode;
      if (!selectedNode ||
          PlacesUtils.getConcreteItemId(selectedNode) != container)
        this._folderTree.selectItems([container]);
    }
  },

  onFolderTreeSelect: function EIO_onFolderTreeSelect() {
    var selectedNode = this._folderTree.selectedNode;

    // Disable the "New Folder" button if we cannot create a new folder
    this._element("newFolderButton")
        .disabled = !this._folderTree.insertionPoint || !selectedNode;

    if (!selectedNode)
      return;

    var folderId = PlacesUtils.getConcreteItemId(selectedNode);
    if (this._getFolderIdFromMenuList() == folderId)
      return;

    var folderItem = this._getFolderMenuItem(folderId);
    this._folderMenuList.selectedItem = folderItem;
    folderItem.doCommand();
  },

  _markFolderAsRecentlyUsed:
  function EIO__markFolderAsRecentlyUsed(aFolderId) {
    var txns = [];

    // Expire old unused recent folders
    var anno = this._getLastUsedAnnotationObject(false);
    while (this._recentFolders.length > MAX_FOLDER_ITEM_IN_MENU_LIST) {
      var folderId = this._recentFolders.pop().folderId;
      let annoTxn = new PlacesSetItemAnnotationTransaction(folderId, anno);
      txns.push(annoTxn);
    }

    // Mark folder as recently used
    anno = this._getLastUsedAnnotationObject(true);
    let annoTxn = new PlacesSetItemAnnotationTransaction(aFolderId, anno);
    txns.push(annoTxn);

    let aggregate = new PlacesAggregatedTransaction("Update last used folders", txns);
    PlacesUtils.transactionManager.doTransaction(aggregate);
  },

  /**
   * Returns an object which could then be used to set/unset the
   * LAST_USED_ANNO annotation for a folder.
   *
   * @param aLastUsed
   *        Whether to set or unset the LAST_USED_ANNO annotation.
   * @returns an object representing the annotation which could then be used
   *          with the transaction manager.
   */
  _getLastUsedAnnotationObject:
  function EIO__getLastUsedAnnotationObject(aLastUsed) {
    var anno = { name: LAST_USED_ANNO,
                 type: Components.interfaces.nsIAnnotationService.TYPE_INT32,
                 flags: 0,
                 value: aLastUsed ? new Date().getTime() : null,
                 expires: Components.interfaces.nsIAnnotationService.EXPIRE_NEVER };

    return anno;
  },

  _rebuildTagsSelectorList: function EIO__rebuildTagsSelectorList() {
    var tagsSelector = this._element("tagsSelector");
    var tagsSelectorRow = this._element("tagsSelectorRow");
    if (tagsSelectorRow.collapsed)
      return;

    while (tagsSelector.hasChildNodes())
      tagsSelector.lastChild.remove();

    var tagsInField = this._getTagsArrayFromTagField();
    var allTags = PlacesUtils.tagging.allTags;
    for (var i = 0; i < allTags.length; i++) {
      var tag = allTags[i];
      var elt = document.createElement("listitem");
      elt.setAttribute("type", "checkbox");
      elt.setAttribute("label", tag);
      if (tagsInField.indexOf(tag) != -1)
        elt.setAttribute("checked", "true");

      tagsSelector.appendChild(elt);
    }
  },

  toggleTagsSelector: function EIO_toggleTagsSelector() {
    var tagsSelector = this._element("tagsSelector");
    var tagsSelectorRow = this._element("tagsSelectorRow");
    var expander = this._element("tagsSelectorExpander");
    if (tagsSelectorRow.collapsed) {
      expander.className = "expander-up";
      expander.setAttribute("tooltiptext",
                            expander.getAttribute("tooltiptextup"));
      tagsSelectorRow.collapsed = false;
      this._rebuildTagsSelectorList();

      // This is a no-op if we've added the listener.
      tagsSelector.addEventListener("CheckboxStateChange", this, false);
    }
    else {
      expander.className = "expander-down";
      expander.setAttribute("tooltiptext",
                            expander.getAttribute("tooltiptextdown"));
      tagsSelectorRow.collapsed = true;
    }
  },

  /**
   * Splits "tagsField" element value, returning an array of valid tag strings.
   *
   * @return Array of tag strings found in the field value.
   */
  _getTagsArrayFromTagField: function EIO__getTagsArrayFromTagField() {
    let tags = this._element("tagsField").value;
    return tags.trim()
               .split(/\s*,\s*/) // Split on commas and remove spaces.
               .filter(tag => tag.length > 0); // Kill empty tags.
  },

  newFolder: function EIO_newFolder() {
    var ip = this._folderTree.insertionPoint;

    // default to the bookmarks menu folder
    if (!ip || ip.itemId == PlacesUIUtils.allBookmarksFolderId) {
        ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
                                PlacesUtils.bookmarks.DEFAULT_INDEX,
                                Components.interfaces.nsITreeView.DROP_ON);
    }

    // XXXmano: add a separate "New Folder" string at some point...
    var defaultLabel = this._element("newFolderButton").label;
    var txn = new PlacesCreateFolderTransaction(defaultLabel, ip.itemId, ip.index);
    PlacesUtils.transactionManager.doTransaction(txn);
    this._folderTree.focus();
    this._folderTree.selectItems([this._lastNewItem]);
    this._folderTree.startEditing(this._folderTree.view.selection.currentIndex,
                                  this._folderTree.columns.getFirstColumn());
  },

  // nsIDOMEventListener
  handleEvent: function EIO_nsIDOMEventListener(aEvent) {
    switch (aEvent.type) {
    case "CheckboxStateChange":
      // Update the tags field when items are checked/unchecked in the listbox
      var tags = this._getTagsArrayFromTagField();

      if (aEvent.target.checked) {
        if (tags.indexOf(aEvent.target.label) == -1)
          tags.push(aEvent.target.label);
      }
      else {
        var indexOfItem = tags.indexOf(aEvent.target.label);
        if (indexOfItem != -1)
          tags.splice(indexOfItem, 1);
      }
      this._element("tagsField").value = tags.join(", ");
      this._updateTags();
      break;
    case "unload":
      this.uninitPanel(false);
      break;
    }
  },

  // nsINavBookmarkObserver
  onItemChanged: function EIO_onItemChanged(aItemId, aProperty,
                                            aIsAnnotationProperty, aValue,
                                            aLastModified, aItemType) {
    if (aProperty == "tags") {
      // Tags case is special, since they should be updated if either:
      // - the notification is for the edited bookmark
      // - the notification is for the edited history entry
      // - the notification is for one of edited uris
      let shouldUpdateTagsField = this._itemId == aItemId;
      if (this._itemId == -1 || this._multiEdit) {
        // Check if the changed uri is part of the modified ones.
        let changedURI = PlacesUtils.bookmarks.getBookmarkURI(aItemId);
        let uris = this._multiEdit ? this._uris : [this._uri];
        uris.forEach(function (aURI, aIndex) {
          if (aURI.equals(changedURI)) {
            shouldUpdateTagsField = true;
            if (this._multiEdit) {
              this._tags[aIndex] = PlacesUtils.tagging.getTagsForURI(this._uris[aIndex]);
            }
          }
        }, this);
      }

      if (shouldUpdateTagsField) {
        if (this._multiEdit) {
          this._allTags = this._getCommonTags();
          this._initTextField("tagsField", this._allTags.join(", "), false);
        }
        else {
          let tags = PlacesUtils.tagging.getTagsForURI(this._uri).join(", ");
          this._initTextField("tagsField", tags, false);
        }
      }

      // Any tags change should be reflected in the tags selector.
      this._rebuildTagsSelectorList();
      return;
    }

    if (this._itemId != aItemId) {
      if (aProperty == "title") {
        // If the title of a folder which is listed within the folders
        // menulist has been changed, we need to update the label of its
        // representing element.
        var menupopup = this._folderMenuList.menupopup;
        for (let i = 0; i < menupopup.childNodes.length; i++) {
          if ("folderId" in menupopup.childNodes[i] &&
              menupopup.childNodes[i].folderId == aItemId) {
            menupopup.childNodes[i].label = aValue;
            break;
          }
        }
      }

      return;
    }

    switch (aProperty) {
    case "title":
      var namePicker = this._element("namePicker");
      if (namePicker.value != aValue) {
        namePicker.value = aValue;
        // clear undo stack
        namePicker.editor.transactionManager.clear();
      }
      break;
    case "uri":
      var locationField = this._element("locationField");
      if (locationField.value != aValue) {
        this._uri = Components.classes["@mozilla.org/network/io-service;1"]
                              .getService(Components.interfaces.nsIIOService)
                              .newURI(aValue, null, null);
        this._initTextField("locationField", this._uri.spec);
        this._initNamePicker();
        this._initTextField("tagsField",
                             PlacesUtils.tagging
                                        .getTagsForURI(this._uri).join(", "),
                            false);
        this._rebuildTagsSelectorList();
      }
      break;
    case "keyword":
      this._initTextField("keywordField",
                          PlacesUtils.bookmarks
                                     .getKeywordForBookmark(this._itemId));
      break;
    case PlacesUIUtils.DESCRIPTION_ANNO:
      this._initTextField("descriptionField",
                          PlacesUIUtils.getItemDescription(this._itemId));
      break;
    case PlacesUtils.LMANNO_FEEDURI:
      var feedURISpec = PlacesUtils.annotations.getItemAnnotation(this._itemId,
                        PlacesUtils.LMANNO_FEEDURI);
      this._initTextField("feedLocationField", feedURISpec, true);
      break;
    case PlacesUtils.LMANNO_SITEURI:
      var siteURISpec = "";
      try {
        siteURISpec = PlacesUtils.annotations.getItemAnnotation(this._itemId,
                      PlacesUtils.LMANNO_SITEURI);
      } catch (e) {}
      this._initTextField("siteLocationField", siteURISpec, true);
      break;
    }
  },

  onItemMoved: function EIO_onItemMoved(aItemId, aOldParent, aOldIndex,
                                        aNewParent, aNewIndex, aItemType) {
    if (aItemId != this._itemId ||
        aNewParent == this._getFolderIdFromMenuList())
      return;

    var folderItem = this._getFolderMenuItem(aNewParent);

    // just setting selectItem _does not_ trigger oncommand, so we don't
    // recurse
    this._folderMenuList.selectedItem = folderItem;
  },

  onItemAdded: function EIO_onItemAdded(aItemId, aParentId, aIndex, aItemType,
                                        aURI) {
    this._lastNewItem = aItemId;
  },

  onItemRemoved: function() { },
  onBeginUpdateBatch: function() { },
  onEndUpdateBatch: function() { },
  onItemVisited: function() { },
};
