/*
 * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved.
 * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com>
 * Copyright (C) 2009 Joseph Pecoraro
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1.  Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 * 2.  Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
 *     its contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * @implements {UI.Searchable}
 * @implements {SDK.SDKModelObserver<!SDK.DOMModel>}
 * @implements {UI.ViewLocationResolver}
 * @unrestricted
 */
Elements.ElementsPanel = class extends UI.Panel {
  constructor() {
    super('elements');
    this.registerRequiredCSS('elements/elementsPanel.css');

    this._splitWidget = new UI.SplitWidget(true, true, 'elementsPanelSplitViewState', 325, 325);
    this._splitWidget.addEventListener(
        UI.SplitWidget.Events.SidebarSizeChanged, this._updateTreeOutlineVisibleWidth.bind(this));
    this._splitWidget.show(this.element);

    this._searchableView = new UI.SearchableView(this);
    this._searchableView.setMinimumSize(25, 28);
    this._searchableView.setPlaceholder(Common.UIString('Find by string, selector, or XPath'));
    const stackElement = this._searchableView.element;

    this._contentElement = createElement('div');

    //fixed by xxxxxx
    var tipsElement = createElement('div');
    var tips = '此处仅支持nvue页面查看元素。vue页面需使用v3编译模式，并且在 菜单视图-显示webview调试 中寻找手机端已经启动的页面进行调试'
    tipsElement.setAttribute('style', 'overflow: hidden;text-overflow: ellipsis;white-space: nowrap;flex: 0 0 27px;border-bottom: 1px solid #ccc;height: 26px;line-height: 26px;margin-top: -2px;padding-left: 15px;background-color: var(--toolbar-bg-color);');
    tipsElement.setAttribute('title', tips)
    tipsElement.innerText = tips
    this._contentElement.appendChild(tipsElement);

    const crumbsContainer = createElement('div');
    stackElement.appendChild(this._contentElement);
    stackElement.appendChild(crumbsContainer);

    this._splitWidget.setMainWidget(this._searchableView);
    /** @type {?Elements.ElementsPanel._splitMode} */
    this._splitMode = null;

    this._contentElement.id = 'elements-content';
    // FIXME: crbug.com/425984
    if (Common.moduleSetting('domWordWrap').get())
      this._contentElement.classList.add('elements-wrap');
    Common.moduleSetting('domWordWrap').addChangeListener(this._domWordWrapSettingChanged.bind(this));

    crumbsContainer.id = 'elements-crumbs';
    this._breadcrumbs = new Elements.ElementsBreadcrumbs();
    this._breadcrumbs.show(crumbsContainer);
    this._breadcrumbs.addEventListener(Elements.ElementsBreadcrumbs.Events.NodeSelected, this._crumbNodeSelected, this);

    this._stylesWidget = new Elements.StylesSidebarPane();
    this._computedStyleWidget = new Elements.ComputedStyleWidget();
    this._metricsWidget = new Elements.MetricsSidebarPane();

    Common.moduleSetting('sidebarPosition').addChangeListener(this._updateSidebarPosition.bind(this));
    this._updateSidebarPosition();

    /** @type {!Array.<!Elements.ElementsTreeOutline>} */
    this._treeOutlines = [];
    /** @type {!Map<!Elements.ElementsTreeOutline, !Element>} */
    this._treeOutlineHeaders = new Map();
    SDK.targetManager.observeModels(SDK.DOMModel, this);
    SDK.targetManager.addEventListener(
        SDK.TargetManager.Events.NameChanged,
        event => this._targetNameChanged(/** @type {!SDK.Target} */ (event.data)));
    Common.moduleSetting('showUAShadowDOM').addChangeListener(this._showUAShadowDOMChanged.bind(this));
    SDK.targetManager.addModelListener(
        SDK.DOMModel, SDK.DOMModel.Events.DocumentUpdated, this._documentUpdatedEvent, this);
    Extensions.extensionServer.addEventListener(
        Extensions.ExtensionServer.Events.SidebarPaneAdded, this._extensionSidebarPaneAdded, this);
  }

  /**
   * @return {!Elements.ElementsPanel}
   */
  static instance() {
    return /** @type {!Elements.ElementsPanel} */ (self.runtime.sharedInstance(Elements.ElementsPanel));
  }

  /**
   * @param {!SDK.CSSProperty} cssProperty
   */
  _revealProperty(cssProperty) {
    return this.sidebarPaneView.showView(this._stylesViewToReveal).then(() => {
      this._stylesWidget.revealProperty(/** @type {!SDK.CSSProperty} */ (cssProperty));
    });
  }

  /**
   * @override
   * @param {string} locationName
   * @return {?UI.ViewLocation}
   */
  resolveLocation(locationName) {
    return this.sidebarPaneView;
  }

  /**
   * @param {?UI.Widget} widget
   * @param {?UI.ToolbarToggle} toggle
   */
  showToolbarPane(widget, toggle) {
    // TODO(luoe): remove this function once its providers have an alternative way to reveal their views.
    this._stylesWidget.showToolbarPane(widget, toggle);
  }

  /**
   * @override
   * @param {!SDK.DOMModel} domModel
   */
  modelAdded(domModel) {
    const parentModel = domModel.parentModel();
    let treeOutline = parentModel ? Elements.ElementsTreeOutline.forDOMModel(parentModel) : null;
    if (!treeOutline) {
      treeOutline = new Elements.ElementsTreeOutline(true, true);
      treeOutline.setWordWrap(Common.moduleSetting('domWordWrap').get());
      treeOutline.addEventListener(
          Elements.ElementsTreeOutline.Events.SelectedNodeChanged, this._selectedNodeChanged, this);
      treeOutline.addEventListener(
          Elements.ElementsTreeOutline.Events.ElementsTreeUpdated, this._updateBreadcrumbIfNeeded, this);
      new Elements.ElementsTreeElementHighlighter(treeOutline);
      this._treeOutlines.push(treeOutline);
      if (domModel.target().parentTarget()) {
        this._treeOutlineHeaders.set(treeOutline, createElementWithClass('div', 'elements-tree-header'));
        this._targetNameChanged(domModel.target());
      }
    }
    treeOutline.wireToDOMModel(domModel);

    // Perform attach if necessary.
    if (this.isShowing())
      this.wasShown();
  }

  /**
   * @override
   * @param {!SDK.DOMModel} domModel
   */
  modelRemoved(domModel) {
    const treeOutline = Elements.ElementsTreeOutline.forDOMModel(domModel);
    treeOutline.unwireFromDOMModel(domModel);
    if (domModel.parentModel())
      return;
    this._treeOutlines.remove(treeOutline);
    const header = this._treeOutlineHeaders.get(treeOutline);
    if (header)
      header.remove();
    this._treeOutlineHeaders.delete(treeOutline);
    treeOutline.element.remove();
  }

  /**
   * @param {!SDK.Target} target
   */
  _targetNameChanged(target) {
    const domModel = target.model(SDK.DOMModel);
    if (!domModel)
      return;
    const treeOutline = Elements.ElementsTreeOutline.forDOMModel(domModel);
    if (!treeOutline)
      return;
    const header = this._treeOutlineHeaders.get(treeOutline);
    if (!header)
      return;
    header.removeChildren();
    header.createChild('div', 'elements-tree-header-frame').textContent = Common.UIString('Frame');
    header.appendChild(Components.Linkifier.linkifyURL(target.inspectedURL(), {text: target.name()}));
  }

  _updateTreeOutlineVisibleWidth() {
    if (!this._treeOutlines.length)
      return;

    let width = this._splitWidget.element.offsetWidth;
    if (this._splitWidget.isVertical())
      width -= this._splitWidget.sidebarSize();
    for (let i = 0; i < this._treeOutlines.length; ++i)
      this._treeOutlines[i].setVisibleWidth(width);

    this._breadcrumbs.updateSizes();
  }

  /**
   * @override
   */
  focus() {
    if (this._treeOutlines.length)
      this._treeOutlines[0].focus();
  }

  /**
   * @override
   * @return {!UI.SearchableView}
   */
  searchableView() {
    return this._searchableView;
  }

  /**
   * @override
   */
  wasShown() {
    UI.context.setFlavor(Elements.ElementsPanel, this);

    for (let i = 0; i < this._treeOutlines.length; ++i) {
      const treeOutline = this._treeOutlines[i];
      // Attach heavy component lazily
      if (treeOutline.element.parentElement !== this._contentElement) {
        const header = this._treeOutlineHeaders.get(treeOutline);
        if (header)
          this._contentElement.appendChild(header);
        this._contentElement.appendChild(treeOutline.element);
      }
    }
    super.wasShown();
    this._breadcrumbs.update();

    const domModels = SDK.targetManager.models(SDK.DOMModel);
    for (const domModel of domModels) {
      if (domModel.parentModel())
        continue;
      const treeOutline = Elements.ElementsTreeOutline.forDOMModel(domModel);
      treeOutline.setVisible(true);

      if (!treeOutline.rootDOMNode) {
        if (domModel.existingDocument()) {
          treeOutline.rootDOMNode = domModel.existingDocument();
          this._documentUpdated(domModel);
        } else {
          domModel.requestDocument();
        }
      }
    }
  }

  /**
   * @override
   */
  willHide() {
    SDK.OverlayModel.hideDOMNodeHighlight();
    for (let i = 0; i < this._treeOutlines.length; ++i) {
      const treeOutline = this._treeOutlines[i];
      treeOutline.setVisible(false);
      // Detach heavy component on hide
      this._contentElement.removeChild(treeOutline.element);
      const header = this._treeOutlineHeaders.get(treeOutline);
      if (header)
        this._contentElement.removeChild(header);
    }
    if (this._popoverHelper)
      this._popoverHelper.hidePopover();
    super.willHide();
  }

  /**
   * @override
   */
  onResize() {
    this.element.window().requestAnimationFrame(this._updateSidebarPosition.bind(this));  // Do not force layout.
    this._updateTreeOutlineVisibleWidth();
  }

  /**
   * @param {!Common.Event} event
   */
  _selectedNodeChanged(event) {
    const selectedNode = /** @type {?SDK.DOMNode} */ (event.data.node);
    const focus = /** @type {boolean} */ (event.data.focus);
    for (const treeOutline of this._treeOutlines) {
      if (!selectedNode || Elements.ElementsTreeOutline.forDOMModel(selectedNode.domModel()) !== treeOutline)
        treeOutline.selectDOMNode(null);
    }

    this._breadcrumbs.setSelectedNode(selectedNode);

    UI.context.setFlavor(SDK.DOMNode, selectedNode);

    if (!selectedNode)
      return;
    selectedNode.setAsInspectedNode();
    if (focus) {
      this._selectedNodeOnReset = selectedNode;
      this._hasNonDefaultSelectedNode = true;
    }

    const executionContexts = selectedNode.domModel().runtimeModel().executionContexts();
    const nodeFrameId = selectedNode.frameId();
    for (const context of executionContexts) {
      if (context.frameId === nodeFrameId) {
        UI.context.setFlavor(SDK.ExecutionContext, context);
        break;
      }
    }
  }

  /**
   * @param {!Common.Event} event
   */
  _documentUpdatedEvent(event) {
    const domModel = /** @type {!SDK.DOMModel} */ (event.data);
    this._documentUpdated(domModel);
  }

  /**
   * @param {!SDK.DOMModel} domModel
   */
  _documentUpdated(domModel) {
    this._searchableView.resetSearch();

    if (!domModel.existingDocument()) {
      if (this.isShowing())
        domModel.requestDocument();
      return;
    }

    this._hasNonDefaultSelectedNode = false;

    if (this._omitDefaultSelection)
      return;

    const savedSelectedNodeOnReset = this._selectedNodeOnReset;
    restoreNode.call(this, domModel, this._selectedNodeOnReset);

    /**
     * @param {!SDK.DOMModel} domModel
     * @param {?SDK.DOMNode} staleNode
     * @this {Elements.ElementsPanel}
     */
    async function restoreNode(domModel, staleNode) {
      const nodePath = staleNode ? staleNode.path() : null;

      const restoredNodeId = nodePath ? await domModel.pushNodeByPathToFrontend(nodePath) : null;

      if (savedSelectedNodeOnReset !== this._selectedNodeOnReset)
        return;
      let node = restoredNodeId ? domModel.nodeForId(restoredNodeId) : null;
      if (!node) {
        const inspectedDocument = domModel.existingDocument();
        node = inspectedDocument ? inspectedDocument.body || inspectedDocument.documentElement : null;
      }
      this._setDefaultSelectedNode(node);
      this._lastSelectedNodeSelectedForTest();
    }
  }

  _lastSelectedNodeSelectedForTest() {
  }

  /**
   * @param {?SDK.DOMNode} node
   */
  _setDefaultSelectedNode(node) {
    if (!node || this._hasNonDefaultSelectedNode || this._pendingNodeReveal)
      return;
    const treeOutline = Elements.ElementsTreeOutline.forDOMModel(node.domModel());
    if (!treeOutline)
      return;
    this.selectDOMNode(node);
    if (treeOutline.selectedTreeElement)
      treeOutline.selectedTreeElement.expand();
  }

  /**
   * @override
   */
  searchCanceled() {
    delete this._searchConfig;
    this._hideSearchHighlights();

    this._searchableView.updateSearchMatchesCount(0);

    delete this._currentSearchResultIndex;
    delete this._searchResults;

    SDK.DOMModel.cancelSearch();
  }

  /**
   * @override
   * @param {!UI.SearchableView.SearchConfig} searchConfig
   * @param {boolean} shouldJump
   * @param {boolean=} jumpBackwards
   */
  performSearch(searchConfig, shouldJump, jumpBackwards) {
    const query = searchConfig.query;

    const whitespaceTrimmedQuery = query.trim();
    if (!whitespaceTrimmedQuery.length)
      return;

    if (!this._searchConfig || this._searchConfig.query !== query)
      this.searchCanceled();
    else
      this._hideSearchHighlights();

    this._searchConfig = searchConfig;

    const showUAShadowDOM = Common.moduleSetting('showUAShadowDOM').get();
    const domModels = SDK.targetManager.models(SDK.DOMModel);
    const promises = domModels.map(domModel => domModel.performSearch(whitespaceTrimmedQuery, showUAShadowDOM));
    Promise.all(promises).then(resultCountCallback.bind(this));

    /**
     * @param {!Array.<number>} resultCounts
     * @this {Elements.ElementsPanel}
     */
    function resultCountCallback(resultCounts) {
      /**
       * @type {!Array.<{domModel: !SDK.DOMModel, index: number, node: (?SDK.DOMNode|undefined)}>}
       */
      this._searchResults = [];
      for (let i = 0; i < resultCounts.length; ++i) {
        const resultCount = resultCounts[i];
        for (let j = 0; j < resultCount; ++j)
          this._searchResults.push({domModel: domModels[i], index: j, node: undefined});
      }
      this._searchableView.updateSearchMatchesCount(this._searchResults.length);
      if (!this._searchResults.length)
        return;
      if (this._currentSearchResultIndex >= this._searchResults.length)
        this._currentSearchResultIndex = undefined;

      let index = this._currentSearchResultIndex;

      if (shouldJump) {
        if (this._currentSearchResultIndex === undefined)
          index = jumpBackwards ? -1 : 0;
        else
          index = jumpBackwards ? index - 1 : index + 1;
        this._jumpToSearchResult(index);
      }
    }
  }

  _domWordWrapSettingChanged(event) {
    // FIXME: crbug.com/425984
    this._contentElement.classList.toggle('elements-wrap', event.data);
    for (let i = 0; i < this._treeOutlines.length; ++i)
      this._treeOutlines[i].setWordWrap(/** @type {boolean} */ (event.data));
  }

  switchToAndFocus(node) {
    // Reset search restore.
    this._searchableView.cancelSearch();
    UI.viewManager.showView('elements').then(() => this.selectDOMNode(node, true));
  }

  /**
   * @param {!Event} event
   * @return {?UI.PopoverRequest}
   */
  _getPopoverRequest(event) {
    let link = event.target;
    while (link && !link[Elements.ElementsTreeElement.HrefSymbol])
      link = link.parentElementOrShadowHost();
    if (!link)
      return null;

    return {
      box: link.boxInWindow(),
      show: async popover => {
        const node = this.selectedDOMNode();
        if (!node)
          return false;
        const preview = await BrowserComponents.ImagePreview.build(
            node.domModel().target(), link[Elements.ElementsTreeElement.HrefSymbol], true);
        if (preview)
          popover.contentElement.appendChild(preview);
        return !!preview;
      }
    };
  }

  _jumpToSearchResult(index) {
    this._currentSearchResultIndex = (index + this._searchResults.length) % this._searchResults.length;
    this._highlightCurrentSearchResult();
  }

  /**
   * @override
   */
  jumpToNextSearchResult() {
    if (!this._searchResults)
      return;
    this.performSearch(this._searchConfig, true);
  }

  /**
   * @override
   */
  jumpToPreviousSearchResult() {
    if (!this._searchResults)
      return;
    this.performSearch(this._searchConfig, true, true);
  }

  /**
   * @override
   * @return {boolean}
   */
  supportsCaseSensitiveSearch() {
    return false;
  }

  /**
   * @override
   * @return {boolean}
   */
  supportsRegexSearch() {
    return false;
  }

  _highlightCurrentSearchResult() {
    const index = this._currentSearchResultIndex;
    const searchResults = this._searchResults;
    const searchResult = searchResults[index];

    this._searchableView.updateCurrentMatchIndex(index);
    if (searchResult.node === null)
      return;

    if (typeof searchResult.node === 'undefined') {
      // No data for slot, request it.
      searchResult.domModel.searchResult(searchResult.index).then(node => {
        searchResult.node = node;
        this._highlightCurrentSearchResult();
      });
      return;
    }

    const treeElement = this._treeElementForNode(searchResult.node);
    searchResult.node.scrollIntoView();
    if (treeElement) {
      treeElement.highlightSearchResults(this._searchConfig.query);
      treeElement.reveal();
      const matches = treeElement.listItemElement.getElementsByClassName(UI.highlightedSearchResultClassName);
      if (matches.length)
        matches[0].scrollIntoViewIfNeeded(false);
    }
  }

  _hideSearchHighlights() {
    if (!this._searchResults || !this._searchResults.length || this._currentSearchResultIndex === undefined)
      return;
    const searchResult = this._searchResults[this._currentSearchResultIndex];
    if (!searchResult.node)
      return;
    const treeOutline = Elements.ElementsTreeOutline.forDOMModel(searchResult.node.domModel());
    const treeElement = treeOutline.findTreeElement(searchResult.node);
    if (treeElement)
      treeElement.hideSearchHighlights();
  }

  /**
   * @return {?SDK.DOMNode}
   */
  selectedDOMNode() {
    for (let i = 0; i < this._treeOutlines.length; ++i) {
      const treeOutline = this._treeOutlines[i];
      if (treeOutline.selectedDOMNode())
        return treeOutline.selectedDOMNode();
    }
    return null;
  }

  /**
   * @param {!SDK.DOMNode} node
   * @param {boolean=} focus
   */
  selectDOMNode(node, focus) {
    for (const treeOutline of this._treeOutlines) {
      const outline = Elements.ElementsTreeOutline.forDOMModel(node.domModel());
      if (outline === treeOutline)
        treeOutline.selectDOMNode(node, focus);
      else
        treeOutline.selectDOMNode(null);
    }
  }

  /**
   * @param {!Common.Event} event
   */
  _updateBreadcrumbIfNeeded(event) {
    const nodes = /** @type {!Array.<!SDK.DOMNode>} */ (event.data);
    this._breadcrumbs.updateNodes(nodes);
  }

  /**
   * @param {!Common.Event} event
   */
  _crumbNodeSelected(event) {
    const node = /** @type {!SDK.DOMNode} */ (event.data);
    this.selectDOMNode(node, true);
  }

  /**
   * @param {?SDK.DOMNode} node
   * @return {?Elements.ElementsTreeOutline}
   */
  _treeOutlineForNode(node) {
    if (!node)
      return null;
    return Elements.ElementsTreeOutline.forDOMModel(node.domModel());
  }

  /**
   * @param {!SDK.DOMNode} node
   * @return {?Elements.ElementsTreeElement}
   */
  _treeElementForNode(node) {
    const treeOutline = this._treeOutlineForNode(node);
    return /** @type {?Elements.ElementsTreeElement} */ (treeOutline.findTreeElement(node));
  }

  /**
   * @param {!SDK.DOMNode} node
   * @return {!SDK.DOMNode}
   */
  _leaveUserAgentShadowDOM(node) {
    let userAgentShadowRoot;
    while ((userAgentShadowRoot = node.ancestorUserAgentShadowRoot()) && userAgentShadowRoot.parentNode)
      node = userAgentShadowRoot.parentNode;
    return node;
  }

  /**
   * @param {!SDK.DOMNode} node
   * @param {boolean} focus
   * @return {!Promise}
   */
  revealAndSelectNode(node, focus) {
    if (Elements.inspectElementModeController && Elements.inspectElementModeController.isInInspectElementMode())
      Elements.inspectElementModeController.stopInspection();

    this._omitDefaultSelection = true;

    node = Common.moduleSetting('showUAShadowDOM').get() ? node : this._leaveUserAgentShadowDOM(node);
    node.highlightForTwoSeconds();

    return UI.viewManager.showView('elements', false, !focus).then(() => {
      this.selectDOMNode(node, focus);
      delete this._omitDefaultSelection;

      if (!this._notFirstInspectElement) {
        Elements.ElementsPanel._firstInspectElementNodeNameForTest = node.nodeName();
        Elements.ElementsPanel._firstInspectElementCompletedForTest();
        InspectorFrontendHost.inspectElementCompleted();
      }
      this._notFirstInspectElement = true;
    });
  }

  _showUAShadowDOMChanged() {
    for (let i = 0; i < this._treeOutlines.length; ++i)
      this._treeOutlines[i].update();
  }

  _updateSidebarPosition() {
    if (this.sidebarPaneView && this.sidebarPaneView.tabbedPane().shouldHideOnDetach())
      return;  // We can't reparent extension iframes.

    let splitMode;
    const position = Common.moduleSetting('sidebarPosition').get();
    if (position === 'right' || (position === 'auto' && UI.inspectorView.element.offsetWidth > 680))
      splitMode = Elements.ElementsPanel._splitMode.Vertical;
    else if (UI.inspectorView.element.offsetWidth > 415)
      splitMode = Elements.ElementsPanel._splitMode.Horizontal;
    else
      splitMode = Elements.ElementsPanel._splitMode.Slim;

    if (this.sidebarPaneView && splitMode === this._splitMode)
      return;
    this._splitMode = splitMode;

    const extensionSidebarPanes = Extensions.extensionServer.sidebarPanes();
    let lastSelectedTabId = null;
    if (this.sidebarPaneView) {
      lastSelectedTabId = this.sidebarPaneView.tabbedPane().selectedTabId;
      this.sidebarPaneView.tabbedPane().detach();
      this._splitWidget.uninstallResizer(this.sidebarPaneView.tabbedPane().headerElement());
    }

    this._splitWidget.setVertical(this._splitMode === Elements.ElementsPanel._splitMode.Vertical);
    this.showToolbarPane(null /* widget */, null /* toggle */);

    const matchedStylePanesWrapper = new UI.VBox();
    matchedStylePanesWrapper.element.classList.add('style-panes-wrapper');
    this._stylesWidget.show(matchedStylePanesWrapper.element);

    const computedStylePanesWrapper = new UI.VBox();
    computedStylePanesWrapper.element.classList.add('style-panes-wrapper');
    this._computedStyleWidget.show(computedStylePanesWrapper.element);

    /**
     * @param {boolean} inComputedStyle
     * @this {Elements.ElementsPanel}
     */
    function showMetrics(inComputedStyle) {
      if (inComputedStyle)
        this._metricsWidget.show(computedStylePanesWrapper.element, this._computedStyleWidget.element);
      else
        this._metricsWidget.show(matchedStylePanesWrapper.element);
    }

    /**
     * @param {!Common.Event} event
     * @this {Elements.ElementsPanel}
     */
    function tabSelected(event) {
      const tabId = /** @type {string} */ (event.data.tabId);
      if (tabId === Common.UIString('Computed'))
        showMetrics.call(this, true);
      else if (tabId === Common.UIString('Styles'))
        showMetrics.call(this, false);
    }

    this.sidebarPaneView = UI.viewManager.createTabbedLocation(() => UI.viewManager.showView('elements'));
    const tabbedPane = this.sidebarPaneView.tabbedPane();
    if (this._popoverHelper)
      this._popoverHelper.hidePopover();
    this._popoverHelper = new UI.PopoverHelper(tabbedPane.element, this._getPopoverRequest.bind(this));
    this._popoverHelper.setHasPadding(true);
    this._popoverHelper.setTimeout(0);

    if (this._splitMode !== Elements.ElementsPanel._splitMode.Vertical)
      this._splitWidget.installResizer(tabbedPane.headerElement());

    const stylesView = new UI.SimpleView(Common.UIString('Styles'));
    this.sidebarPaneView.appendView(stylesView);
    if (splitMode === Elements.ElementsPanel._splitMode.Horizontal) {
      // Styles and computed are merged into a single tab.
      stylesView.element.classList.add('flex-auto');

      const splitWidget = new UI.SplitWidget(true, true, 'stylesPaneSplitViewState', 215);
      splitWidget.show(stylesView.element);
      splitWidget.setMainWidget(matchedStylePanesWrapper);
      splitWidget.setSidebarWidget(computedStylePanesWrapper);
    } else {
      // Styles and computed are in separate tabs.
      stylesView.element.classList.add('flex-auto');
      matchedStylePanesWrapper.show(stylesView.element);

      const computedView = new UI.SimpleView(Common.UIString('Computed'));
      computedView.element.classList.add('composite', 'fill');
      computedStylePanesWrapper.show(computedView.element);

      tabbedPane.addEventListener(UI.TabbedPane.Events.TabSelected, tabSelected, this);
      this.sidebarPaneView.appendView(computedView);
    }
    this._stylesViewToReveal = stylesView;

    showMetrics.call(this, this._splitMode === Elements.ElementsPanel._splitMode.Horizontal);

    this.sidebarPaneView.appendApplicableItems('elements-sidebar');
    for (let i = 0; i < extensionSidebarPanes.length; ++i)
      this._addExtensionSidebarPane(extensionSidebarPanes[i]);

    if (lastSelectedTabId)
      this.sidebarPaneView.tabbedPane().selectTab(lastSelectedTabId);

    this._splitWidget.setSidebarWidget(this.sidebarPaneView.tabbedPane());
  }

  /**
   * @param {!Common.Event} event
   */
  _extensionSidebarPaneAdded(event) {
    const pane = /** @type {!Extensions.ExtensionSidebarPane} */ (event.data);
    this._addExtensionSidebarPane(pane);
  }

  /**
   * @param {!Extensions.ExtensionSidebarPane} pane
   */
  _addExtensionSidebarPane(pane) {
    if (pane.panelName() === this.name)
      this.sidebarPaneView.appendView(pane);
  }
};

Elements.ElementsPanel._elementsSidebarViewTitleSymbol = Symbol('title');

/** @enum {symbol} */
Elements.ElementsPanel._splitMode = {
  Vertical: Symbol('Vertical'),
  Horizontal: Symbol('Horizontal'),
  Slim: Symbol('Slim'),
};

// Sniffed in tests.
Elements.ElementsPanel._firstInspectElementCompletedForTest = function() {};

/**
 * @implements {UI.ContextMenu.Provider}
 * @unrestricted
 */
Elements.ElementsPanel.ContextMenuProvider = class {
  /**
   * @override
   * @param {!Event} event
   * @param {!UI.ContextMenu} contextMenu
   * @param {!Object} object
   */
  appendApplicableItems(event, contextMenu, object) {
    if (!(object instanceof SDK.RemoteObject && (/** @type {!SDK.RemoteObject} */ (object)).isNode()) &&
        !(object instanceof SDK.DOMNode) && !(object instanceof SDK.DeferredDOMNode))
      return;

    // Skip adding "Reveal..." menu item for our own tree outline.
    if (Elements.ElementsPanel.instance().element.isAncestor(/** @type {!Node} */ (event.target)))
      return;
    const commandCallback = Common.Revealer.reveal.bind(Common.Revealer, object);
    contextMenu.revealSection().appendItem(Common.UIString('Reveal in Elements panel'), commandCallback);
  }
};

/**
 * @implements {Common.Revealer}
 * @unrestricted
 */
Elements.ElementsPanel.DOMNodeRevealer = class {
  /**
   * @override
   * @param {!Object} node
   * @param {boolean=} omitFocus
   * @return {!Promise}
   */
  reveal(node, omitFocus) {
    const panel = Elements.ElementsPanel.instance();
    panel._pendingNodeReveal = true;

    return new Promise(revealPromise);

    /**
     * @param {function(undefined)} resolve
     * @param {function(!Error)} reject
     */
    function revealPromise(resolve, reject) {
      if (node instanceof SDK.DOMNode) {
        onNodeResolved(/** @type {!SDK.DOMNode} */ (node));
      } else if (node instanceof SDK.DeferredDOMNode) {
        (/** @type {!SDK.DeferredDOMNode} */ (node)).resolve(onNodeResolved);
      } else if (node instanceof SDK.RemoteObject) {
        const domModel = /** @type {!SDK.RemoteObject} */ (node).runtimeModel().target().model(SDK.DOMModel);
        if (domModel)
          domModel.pushObjectAsNodeToFrontend(node).then(onNodeResolved);
        else
          reject(new Error('Could not resolve a node to reveal.'));
      } else {
        reject(new Error('Can\'t reveal a non-node.'));
        panel._pendingNodeReveal = false;
      }

      /**
       * @param {?SDK.DOMNode} resolvedNode
       */
      function onNodeResolved(resolvedNode) {
        panel._pendingNodeReveal = false;

        if (resolvedNode) {
          panel.revealAndSelectNode(resolvedNode, !omitFocus).then(resolve);
          return;
        }
        reject(new Error('Could not resolve node to reveal.'));
      }
    }
  }
};

/**
 * @implements {Common.Revealer}
 * @unrestricted
 */
Elements.ElementsPanel.CSSPropertyRevealer = class {
  /**
   * @override
   * @param {!Object} property
   * @return {!Promise}
   */
  reveal(property) {
    const panel = Elements.ElementsPanel.instance();
    return panel._revealProperty(/** @type {!SDK.CSSProperty} */ (property));
  }
};


/**
 * @implements {UI.ActionDelegate}
 * @unrestricted
 */
Elements.ElementsActionDelegate = class {
  /**
   * @override
   * @param {!UI.Context} context
   * @param {string} actionId
   * @return {boolean}
   */
  handleAction(context, actionId) {
    const node = UI.context.flavor(SDK.DOMNode);
    if (!node)
      return true;
    const treeOutline = Elements.ElementsTreeOutline.forDOMModel(node.domModel());
    if (!treeOutline)
      return true;

    switch (actionId) {
      case 'elements.hide-element':
        treeOutline.toggleHideElement(node);
        return true;
      case 'elements.edit-as-html':
        treeOutline.toggleEditAsHTML(node);
        return true;
      case 'elements.undo':
        if (UI.isEditing())
          return false;
        SDK.domModelUndoStack.undo();
        Elements.ElementsPanel.instance()._stylesWidget.forceUpdate();
        return true;
      case 'elements.redo':
        if (UI.isEditing())
          return false;
        SDK.domModelUndoStack.redo();
        Elements.ElementsPanel.instance()._stylesWidget.forceUpdate();
        return true;
    }
    return false;
  }
};

/**
 * @implements {Elements.MarkerDecorator}
 * @unrestricted
 */
Elements.ElementsPanel.PseudoStateMarkerDecorator = class {
  /**
   * @override
   * @param {!SDK.DOMNode} node
   * @return {?{title: string, color: string}}
   */
  decorate(node) {
    return {
      color: 'orange',
      title: Common.UIString('Element state: %s', ':' + node.domModel().cssModel().pseudoState(node).join(', :'))
    };
  }
};
