/* 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 all the views.
 * It should not be instanced directly, use a derived class instead.
 */
class PlacesViewBase {
  /**
   * @param {string} placesUrl
   *   The query string associated with the view.
   * @param {DOMElement} rootElt
   *   The root element for the view.
   * @param {DOMElement} viewElt
   *   The view element.
   */
  constructor(placesUrl, rootElt, viewElt) {
    this._rootElt = rootElt;
    this._viewElt = viewElt;
    // Do initialization in subclass now that `this` exists.
    this._init?.();
    this._controller = new PlacesController(this);
    this.place = placesUrl;
    this._viewElt.controllers.appendController(this._controller);
  }
  // The xul element that holds the entire view.
  _viewElt = null;
  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;
  static interfaces = [
    Ci.nsINavHistoryResultObserver,
    Ci.nsISupportsWeakReference,
  ];
  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;
    }
  }
  /**
   * 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(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({
      parentGuid: PlacesUtils.getConcreteItemGuid(container),
      index,
      orientation,
      tagName,
    });
  }
  buildContextMenu(aPopup) {
    this._contextMenuShown = aPopup;
    window.updateCommands("places");
    // Ensure that an existing "Show Other Bookmarks" item is removed before adding it
    // again.
    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.
    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");
    }
    return this.controller.buildContextMenu(aPopup);
  }
  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(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(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(aChild) {
    aChild.remove();
  }
  _setEmptyPopupStatus(aPopup, aEmpty) {
    if (!aPopup._emptyMenuitem) {
      aPopup._emptyMenuitem = document.createXULElement("menuitem");
      aPopup._emptyMenuitem.setAttribute("disabled", true);
      aPopup._emptyMenuitem.className = "bookmark-item";
      document.l10n.setAttributes(
        aPopup._emptyMenuitem,
        "places-empty-bookmarks-folder"
      );
    }
    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(aPlacesNode) {
    this._domNodes.delete(aPlacesNode);
    let element;
    let type = aPlacesNode.type;
    if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
      element = document.createXULElement("menuseparator");
    } 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";
        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(aNewChild, aInsertionNode, aBefore = null) {
    let element = this._createDOMNodeForPlacesNode(aNewChild);
    aInsertionNode.insertBefore(element, aBefore);
    return element;
  }
  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(aPlacesNode, aURIString) {
    let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true);
    // There's no DOM node, thus there's nothing to be done when the URI changes.
    if (!elt) {
      return;
    }
    // Here we need the