/* 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/. */
/* eslint-env mozilla/browser-window */
/**
 * The base view implements everything that's common to the toolbar and
 * menu views.
 *
 * @param {string} aPlace
 *   The query string associated with the view.
 * @param {object} aOptions
 *   Associated options for the view.
 */
function PlacesViewBase(aPlace, aOptions = {}) {
  if ("rootElt" in aOptions) {
    this._rootElt = aOptions.rootElt;
  }
  if ("viewElt" in aOptions) {
    this._viewElt = aOptions.viewElt;
  }
  this.options = aOptions;
  this._controller = new PlacesController(this);
  this.place = aPlace;
  this._viewElt.controllers.appendController(this._controller);
}
PlacesViewBase.interfaces = [
  Ci.nsINavHistoryResultObserver,
  Ci.nsISupportsWeakReference,
];
PlacesViewBase.prototype = {
  // The xul element that holds the entire view.
  _viewElt: null,
  get viewElt() {
    return this._viewElt;
  },
  get associatedElement() {
    return this._viewElt;
  },
  get controllers() {
    return this._viewElt.controllers;
  },
  // The xul element that represents the root container.
  _rootElt: null,
  // Set to true for views that are represented by native widgets (i.e.
  // the native mac menu).
  _nativeView: false,
  QueryInterface: ChromeUtils.generateQI(PlacesViewBase.interfaces),
  _place: "",
  get place() {
    return this._place;
  },
  set place(val) {
    this._place = val;
    let history = PlacesUtils.history;
    let query = {},
      options = {};
    history.queryStringToQuery(val, query, options);
    let result = history.executeQuery(query.value, options.value);
    result.addObserver(this);
  },
  _result: null,
  get result() {
    return this._result;
  },
  set result(val) {
    if (this._result == val) {
      return;
    }
    if (this._result) {
      this._result.removeObserver(this);
      this._resultNode.containerOpen = false;
    }
    if (this._rootElt.localName == "menupopup") {
      this._rootElt._built = false;
    }
    this._result = val;
    if (val) {
      this._resultNode = val.root;
      this._rootElt._placesNode = this._resultNode;
      this._domNodes = new Map();
      this._domNodes.set(this._resultNode, this._rootElt);
      // This calls _rebuild through invalidateContainer.
      this._resultNode.containerOpen = true;
    } else {
      this._resultNode = null;
      delete this._domNodes;
    }
  },
  _options: null,
  get options() {
    return this._options;
  },
  set options(val) {
    if (!val) {
      val = {};
    }
    if (!("extraClasses" in val)) {
      val.extraClasses = {};
    }
    this._options = val;
  },
  /**
   * Gets the DOM node used for the given places node.
   *
   * @param {object} aPlacesNode
   *        a places result node.
   * @param {boolean} aAllowMissing
   *        whether the node may be missing
   * @returns {object|null} The associated DOM node.
   * @throws if there is no DOM node set for aPlacesNode.
   */
  _getDOMNodeForPlacesNode: function PVB__getDOMNodeForPlacesNode(
    aPlacesNode,
    aAllowMissing = false
  ) {
    let node = this._domNodes.get(aPlacesNode, null);
    if (!node && !aAllowMissing) {
      throw new Error(
        "No DOM node set for aPlacesNode.\nnode.type: " +
          aPlacesNode.type +
          ". node.parent: " +
          aPlacesNode
      );
    }
    return node;
  },
  get controller() {
    return this._controller;
  },
  get selType() {
    return "single";
  },
  selectItems() {},
  selectAll() {},
  get selectedNode() {
    if (this._contextMenuShown) {
      let anchor = this._contextMenuShown.triggerNode;
      if (!anchor) {
        return null;
      }
      if (anchor._placesNode) {
        return this._rootElt == anchor ? null : anchor._placesNode;
      }
      anchor = anchor.parentNode;
      return this._rootElt == anchor ? null : anchor._placesNode || null;
    }
    return null;
  },
  get hasSelection() {
    return this.selectedNode != null;
  },
  get selectedNodes() {
    let selectedNode = this.selectedNode;
    return selectedNode ? [selectedNode] : [];
  },
  get singleClickOpens() {
    return true;
  },
  get removableSelectionRanges() {
    // On static content the current selectedNode would be the selection's
    // parent node. We don't want to allow removing a node when the
    // selection is not explicit.
    let popupNode = PlacesUIUtils.lastContextMenuTriggerNode;
    if (popupNode && (popupNode == "menupopup" || !popupNode._placesNode)) {
      return [];
    }
    return [this.selectedNodes];
  },
  get draggableSelection() {
    return [this._draggedElt];
  },
  get insertionPoint() {
    // There is no insertion point for history queries, so bail out now and
    // save a lot of work when updating commands.
    let resultNode = this._resultNode;
    if (
      PlacesUtils.nodeIsQuery(resultNode) &&
      PlacesUtils.asQuery(resultNode).queryOptions.queryType ==
        Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY
    ) {
      return null;
    }
    // By default, the insertion point is at the top level, at the end.
    let index = PlacesUtils.bookmarks.DEFAULT_INDEX;
    let container = this._resultNode;
    let orientation = Ci.nsITreeView.DROP_BEFORE;
    let tagName = null;
    let selectedNode = this.selectedNode;
    if (selectedNode) {
      let popupNode = PlacesUIUtils.lastContextMenuTriggerNode;
      if (
        !popupNode._placesNode ||
        popupNode._placesNode == this._resultNode ||
        popupNode._placesNode.itemId == -1 ||
        !selectedNode.parent
      ) {
        // If a static menuitem is selected, or if the root node is selected,
        // the insertion point is inside the folder, at the end.
        container = selectedNode;
        orientation = Ci.nsITreeView.DROP_ON;
      } else {
        // In all other cases the insertion point is before that node.
        container = selectedNode.parent;
        index = container.getChildIndex(selectedNode);
        if (PlacesUtils.nodeIsTagQuery(container)) {
          tagName = PlacesUtils.asQuery(container).query.tags[0];
        }
      }
    }
    if (this.controller.disallowInsertion(container)) {
      return null;
    }
    return new PlacesInsertionPoint({
      parentId: PlacesUtils.getConcreteItemId(container),
      parentGuid: PlacesUtils.getConcreteItemGuid(container),
      index,
      orientation,
      tagName,
    });
  },
  buildContextMenu: function PVB_buildContextMenu(aPopup) {
    this._contextMenuShown = aPopup;
    window.updateCommands("places");
    // Ensure that an existing "Show Other Bookmarks" item is removed before adding it
    // again. This item should only be added when gBookmarksToolbar2h2020 is true, but
    // its possible the pref could be toggled off in the same window. This results in
    // the "Show Other Bookmarks" menu item still being visible even when the pref is
    // set to false.
    let existingOtherBookmarksItem = aPopup.querySelector(
      "#show-other-bookmarks_PersonalToolbar"
    );
    existingOtherBookmarksItem?.remove();
    let manageBookmarksMenu = aPopup.querySelector(
      "#placesContext_showAllBookmarks"
    );
    // Add the View menu for the Bookmarks Toolbar and "Show Other Bookmarks" menu item
    // if the click originated from the Bookmarks Toolbar.
    if (gBookmarksToolbar2h2020) {
      let existingSubmenu = aPopup.querySelector("#toggle_PersonalToolbar");
      existingSubmenu?.remove();
      let bookmarksToolbar = document.getElementById("PersonalToolbar");
      if (bookmarksToolbar?.contains(aPopup.triggerNode)) {
        manageBookmarksMenu.removeAttribute("hidden");
        let menu = BookmarkingUI.buildBookmarksToolbarSubmenu(bookmarksToolbar);
        aPopup.insertBefore(menu, manageBookmarksMenu);
        if (
          aPopup.triggerNode.id === "OtherBookmarks" ||
          aPopup.triggerNode.id === "PlacesChevron" ||
          aPopup.triggerNode.id === "PlacesToolbarItems" ||
          aPopup.triggerNode.parentNode.id === "PlacesToolbarItems"
        ) {
          let otherBookmarksMenuItem = BookmarkingUI.buildShowOtherBookmarksMenuItem();
          if (otherBookmarksMenuItem) {
            aPopup.insertBefore(
              otherBookmarksMenuItem,
              menu.nextElementSibling
            );
          }
        }
      } else {
        manageBookmarksMenu.setAttribute("hidden", "true");
      }
    } else {
      manageBookmarksMenu.setAttribute("hidden", "true");
    }
    return this.controller.buildContextMenu(aPopup);
  },
  destroyContextMenu: function PVB_destroyContextMenu(aPopup) {
    this._contextMenuShown = null;
  },
  clearAllContents(aPopup) {
    let kid = aPopup.firstElementChild;
    while (kid) {
      let next = kid.nextElementSibling;
      if (!kid.classList.contains("panel-header")) {
        kid.remove();
      }
      kid = next;
    }
    aPopup._emptyMenuitem = aPopup._startMarker = aPopup._endMarker = null;
  },
  _cleanPopup: function PVB_cleanPopup(aPopup, aDelay) {
    // Ensure markers are here when `invalidateContainer` is called before the
    // popup is shown, which may the case for panelviews, for example.
    this._ensureMarkers(aPopup);
    // Remove Places nodes from the popup.
    let child = aPopup._startMarker;
    while (child.nextElementSibling != aPopup._endMarker) {
      let sibling = child.nextElementSibling;
      if (sibling._placesNode && !aDelay) {
        aPopup.removeChild(sibling);
      } else if (sibling._placesNode && aDelay) {
        // HACK (bug 733419): the popups originating from the OS X native
        // menubar don't live-update while open, thus we don't clean it
        // until the next popupshowing, to avoid zombie menuitems.
        if (!aPopup._delayedRemovals) {
          aPopup._delayedRemovals = [];
        }
        aPopup._delayedRemovals.push(sibling);
        child = child.nextElementSibling;
      } else {
        child = child.nextElementSibling;
      }
    }
  },
  _rebuildPopup: function PVB__rebuildPopup(aPopup) {
    let resultNode = aPopup._placesNode;
    if (!resultNode.containerOpen) {
      return;
    }
    this._cleanPopup(aPopup);
    let cc = resultNode.childCount;
    if (cc > 0) {
      this._setEmptyPopupStatus(aPopup, false);
      let fragment = document.createDocumentFragment();
      for (let i = 0; i < cc; ++i) {
        let child = resultNode.getChild(i);
        this._insertNewItemToPopup(child, fragment);
      }
      aPopup.insertBefore(fragment, aPopup._endMarker);
    } else {
      this._setEmptyPopupStatus(aPopup, true);
    }
    aPopup._built = true;
  },
  _removeChild: function PVB__removeChild(aChild) {
    aChild.remove();
  },
  _setEmptyPopupStatus: function PVB__setEmptyPopupStatus(aPopup, aEmpty) {
    if (!aPopup._emptyMenuitem) {
      let label = PlacesUIUtils.getString("bookmarksMenuEmptyFolder");
      aPopup._emptyMenuitem = document.createXULElement("menuitem");
      aPopup._emptyMenuitem.setAttribute("label", label);
      aPopup._emptyMenuitem.setAttribute("disabled", true);
      aPopup._emptyMenuitem.className = "bookmark-item";
      if (typeof this.options.extraClasses.entry == "string") {
        aPopup._emptyMenuitem.classList.add(this.options.extraClasses.entry);
      }
    }
    if (aEmpty) {
      aPopup.setAttribute("emptyplacesresult", "true");
      // Don't add the menuitem if there is static content.
      if (
        !aPopup._startMarker.previousElementSibling &&
        !aPopup._endMarker.nextElementSibling
      ) {
        aPopup.insertBefore(aPopup._emptyMenuitem, aPopup._endMarker);
      }
    } else {
      aPopup.removeAttribute("emptyplacesresult");
      try {
        aPopup.removeChild(aPopup._emptyMenuitem);
      } catch (ex) {}
    }
  },
  _createDOMNodeForPlacesNode: function PVB__createDOMNodeForPlacesNode(
    aPlacesNode
  ) {
    this._domNodes.delete(aPlacesNode);
    let element;
    let type = aPlacesNode.type;
    if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
      element = document.createXULElement("menuseparator");
      element.setAttribute("class", "small-separator");
    } else {
      if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI) {
        element = document.createXULElement("menuitem");
        element.className =
          "menuitem-iconic bookmark-item menuitem-with-favicon";
        element.setAttribute(
          "scheme",
          PlacesUIUtils.guessUrlSchemeForUI(aPlacesNode.uri)
        );
      } else if (PlacesUtils.containerTypes.includes(type)) {
        element = document.createXULElement("menu");
        element.setAttribute("container", "true");
        if (aPlacesNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) {
          element.setAttribute("query", "true");
          if (PlacesUtils.nodeIsTagQuery(aPlacesNode)) {
            element.setAttribute("tagContainer", "true");
          } else if (PlacesUtils.nodeIsDay(aPlacesNode)) {
            element.setAttribute("dayContainer", "true");
          } else if (PlacesUtils.nodeIsHost(aPlacesNode)) {
            element.setAttribute("hostContainer", "true");
          }
        }
        let popup = document.createXULElement("menupopup", {
          is: "places-popup",
        });
        popup._placesNode = PlacesUtils.asContainer(aPlacesNode);
        if (!this._nativeView) {
          popup.setAttribute("placespopup", "true");
        }
        element.appendChild(popup);
        element.className = "menu-iconic bookmark-item";
        if (typeof this.options.extraClasses.entry == "string") {
          element.classList.add(this.options.extraClasses.entry);
        }
        this._domNodes.set(aPlacesNode, popup);
      } else {
        throw new Error("Unexpected node");
      }
      element.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode));
      let icon = aPlacesNode.icon;
      if (icon) {
        element.setAttribute("image", icon);
      }
    }
    element._placesNode = aPlacesNode;
    if (!this._domNodes.has(aPlacesNode)) {
      this._domNodes.set(aPlacesNode, element);
    }
    return element;
  },
  _insertNewItemToPopup: function PVB__insertNewItemToPopup(
    aNewChild,
    aInsertionNode,
    aBefore = null
  ) {
    let element = this._createDOMNodeForPlacesNode(aNewChild);
    if (element.localName == "menuitem" || element.localName == "menu") {
      if (typeof this.options.extraClasses.entry == "string") {
        element.classList.add(this.options.extraClasses.entry);
      }
    }
    aInsertionNode.insertBefore(element, aBefore);
    return element;
  },
  toggleCutNode: function PVB_toggleCutNode(aPlacesNode, aValue) {
    let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
    // We may get the popup for menus, but we need the menu itself.
    if (elt.localName == "menupopup") {
      elt = elt.parentNode;
    }
    if (aValue) {
      elt.setAttribute("cutting", "true");
    } else {
      elt.removeAttribute("cutting");
    }
  },
  nodeURIChanged: function PVB_nodeURIChanged(aPlacesNode, aURIString) {
    let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
    // Here we need the