// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/* eslint-disable @devtools/no-imperative-dom-api */

import * as Common from '../../core/common/common.js';
import * as Platform from '../../core/platform/platform.js';
import * as Geometry from '../../models/geometry/geometry.js';
import * as VisualLogging from '../visual_logging/visual_logging.js';

import * as ARIAUtils from './ARIAUtils.js';
import {Events as ResizerWidgetEvents, type ResizeUpdatePositionEvent, SimpleResizerWidget} from './ResizerWidget.js';
import splitWidgetStyles from './splitWidget.css.js';
import {ToolbarButton} from './Toolbar.js';
import {registerWidgetConfig, Widget, widgetConfig, WidgetElement} from './Widget.js';
import {Events as ZoomManagerEvents, ZoomManager} from './ZoomManager.js';

export class SplitWidget extends Common.ObjectWrapper.eventMixin<EventTypes, typeof Widget>(Widget) {
  #sidebarElement: HTMLElement;
  #mainElement: HTMLElement;
  #resizerElement: HTMLElement;
  #resizerElementSize: number|null = null;
  readonly #resizerWidget: SimpleResizerWidget;
  #defaultSidebarWidth: number;
  #defaultSidebarHeight: number;
  readonly #constraintsInDip: boolean;
  #resizeStartSizeDIP = 0;
  // TODO: Used in WebTests
  private setting: Common.Settings.Setting<{
    vertical?: SettingForOrientation,
    horizontal?: SettingForOrientation,
  }>|null;
  #totalSizeCSS = 0;
  #totalSizeOtherDimensionCSS = 0;
  #mainWidget: Widget|null = null;
  #sidebarWidget: Widget|null = null;
  #animationFrameHandle = 0;
  #animationCallback: (() => void)|null = null;
  #showSidebarButtonTitle: Common.UIString.LocalizedString = Common.UIString.LocalizedEmptyString;
  #hideSidebarButtonTitle: Common.UIString.LocalizedString = Common.UIString.LocalizedEmptyString;
  #shownSidebarString: Common.UIString.LocalizedString = Common.UIString.LocalizedEmptyString;
  #hiddenSidebarString: Common.UIString.LocalizedString = Common.UIString.LocalizedEmptyString;
  #showHideSidebarButton: ToolbarButton|null = null;
  #isVertical = false;
  #sidebarMinimized = false;
  #detaching = false;
  #sidebarSizeDIP = -1;
  #savedSidebarSizeDIP: number;
  #secondIsSidebar = false;
  #shouldSaveShowMode = false;
  #savedVerticalMainSize: number|null = null;
  #savedHorizontalMainSize: number|null = null;
  #showMode: ShowMode = ShowMode.BOTH;
  #savedShowMode: ShowMode;
  #autoAdjustOrientation = false;

  constructor(
      isVertical: boolean,
      secondIsSidebar: boolean,
      settingName?: string,
      defaultSidebarWidth?: number,
      defaultSidebarHeight?: number,
      constraintsInDip?: boolean,
      element?: SplitWidgetElement,
  ) {
    super(element, {useShadowDom: true});
    this.element.classList.add('split-widget');
    this.registerRequiredCSS(splitWidgetStyles);

    this.contentElement.classList.add('shadow-split-widget');
    this.#sidebarElement =
        this.contentElement.createChild('div', 'shadow-split-widget-contents shadow-split-widget-sidebar vbox');
    this.#mainElement =
        this.contentElement.createChild('div', 'shadow-split-widget-contents shadow-split-widget-main vbox');
    const mainSlot = this.#mainElement.createChild('slot');
    mainSlot.name = 'main';
    mainSlot.addEventListener('slotchange', (_: Event) => {
      const assignedNode = mainSlot.assignedNodes()[0];
      const widget = assignedNode instanceof HTMLElement ? Widget.getOrCreateWidget(assignedNode) : null;
      if (widget && widget !== this.#mainWidget) {
        this.setMainWidget(widget);
      }
    });
    const sidebarSlot = this.#sidebarElement.createChild('slot');
    sidebarSlot.name = 'sidebar';
    sidebarSlot.addEventListener('slotchange', (_: Event) => {
      const assignedNode = sidebarSlot.assignedNodes()[0];
      const widget = assignedNode instanceof HTMLElement ? Widget.getOrCreateWidget(assignedNode) : null;
      if (widget && widget !== this.#sidebarWidget) {
        this.setSidebarWidget(widget);
      }
    });
    this.#resizerElement = this.contentElement.createChild('div', 'shadow-split-widget-resizer');

    this.#resizerWidget = new SimpleResizerWidget();
    this.#resizerWidget.setEnabled(true);
    this.#resizerWidget.addEventListener(ResizerWidgetEvents.RESIZE_START, this.#onResizeStart, this);
    this.#resizerWidget.addEventListener(ResizerWidgetEvents.RESIZE_UPDATE_POSITION, this.#onResizeUpdate, this);
    this.#resizerWidget.addEventListener(ResizerWidgetEvents.RESIZE_END, this.#onResizeEnd, this);

    this.#defaultSidebarWidth = defaultSidebarWidth || 200;
    this.#defaultSidebarHeight = defaultSidebarHeight || this.#defaultSidebarWidth;
    this.#constraintsInDip = Boolean(constraintsInDip);

    this.setting = settingName ? Common.Settings.Settings.instance().createSetting(settingName, {}) : null;
    this.#savedSidebarSizeDIP = this.#sidebarSizeDIP;

    this.setSecondIsSidebar(secondIsSidebar);

    this.#setVertical(isVertical);
    this.#savedShowMode = this.#showMode;

    // Should be called after isVertical has the right value.
    this.installResizer(this.#resizerElement);
  }

  isVertical(): boolean {
    return this.#isVertical;
  }

  setVertical(isVertical: boolean): void {
    if (this.#isVertical === isVertical) {
      return;
    }

    this.#setVertical(isVertical);

    if (this.isShowing()) {
      this.#updateLayout();
    }
  }

  setAutoAdjustOrientation(autoAdjustOrientation: boolean): void {
    this.#autoAdjustOrientation = autoAdjustOrientation;
    this.#maybeAutoAdjustOrientation();
  }

  #setVertical(isVertical: boolean): void {
    this.contentElement.classList.toggle('vbox', !isVertical);
    this.contentElement.classList.toggle('hbox', isVertical);
    this.#isVertical = isVertical;

    this.#resizerElementSize = null;
    this.#sidebarSizeDIP = -1;
    this.#restoreSidebarSizeFromSettings();
    if (this.#shouldSaveShowMode) {
      this.#restoreAndApplyShowModeFromSettings();
    }
    this.#updateShowHideSidebarButton();
    // FIXME: reverse SplitWidget.isVertical meaning.
    this.#resizerWidget.setVertical(!isVertical);
    this.invalidateConstraints();
  }

  #updateLayout(animate?: boolean): void {
    this.#totalSizeCSS = 0;  // Lazy update.
    this.#totalSizeOtherDimensionCSS = 0;

    // Remove properties that might affect total size calculation.
    this.#mainElement.style.removeProperty('width');
    this.#mainElement.style.removeProperty('height');
    this.#sidebarElement.style.removeProperty('width');
    this.#sidebarElement.style.removeProperty('height');

    this.#setSidebarSizeDIP(this.#preferredSidebarSizeDIP(), Boolean(animate));
  }

  setMainWidget(widget: Widget): void {
    if (this.#mainWidget === widget) {
      return;
    }
    this.suspendInvalidations();
    if (this.#mainWidget) {
      this.#mainWidget.detach();
    }
    this.#mainWidget = widget;
    if (widget) {
      widget.element.slot = 'main';
      if (this.#showMode === ShowMode.ONLY_MAIN || this.#showMode === ShowMode.BOTH) {
        widget.show(this.element);
      }
    }
    this.resumeInvalidations();
  }

  setSidebarWidget(widget: Widget): void {
    if (this.#sidebarWidget === widget) {
      return;
    }
    this.suspendInvalidations();
    if (this.#sidebarWidget) {
      this.#sidebarWidget.detach();
    }
    this.#sidebarWidget = widget;
    if (widget) {
      widget.element.slot = 'sidebar';
      if (this.#showMode === ShowMode.ONLY_SIDEBAR || this.#showMode === ShowMode.BOTH) {
        widget.show(this.element);
      }
    }
    this.resumeInvalidations();
  }

  mainWidget(): Widget|null {
    return this.#mainWidget;
  }

  sidebarWidget(): Widget|null {
    return this.#sidebarWidget;
  }

  sidebarElement(): HTMLElement {
    return this.#sidebarElement;
  }

  override childWasDetached(widget: Widget): void {
    if (this.#detaching) {
      return;
    }
    if (this.#mainWidget === widget) {
      this.#mainWidget = null;
    }
    if (this.#sidebarWidget === widget) {
      this.#sidebarWidget = null;
    }
    this.invalidateConstraints();
  }

  isSidebarSecond(): boolean {
    return this.#secondIsSidebar;
  }

  enableShowModeSaving(): void {
    this.#shouldSaveShowMode = true;
    this.#restoreAndApplyShowModeFromSettings();
  }

  showMode(): ShowMode {
    return this.#showMode;
  }

  sidebarIsShowing(): boolean {
    return this.#showMode !== ShowMode.ONLY_MAIN;
  }

  setSecondIsSidebar(secondIsSidebar: boolean): void {
    if (secondIsSidebar === this.#secondIsSidebar) {
      return;
    }
    this.#secondIsSidebar = secondIsSidebar;
    if (!this.#mainWidget?.shouldHideOnDetach()) {
      if (secondIsSidebar) {
        this.contentElement.insertBefore(this.#mainElement, this.#sidebarElement);
      } else {
        this.contentElement.insertBefore(this.#mainElement, this.#resizerElement);
      }
    } else if (!this.#sidebarWidget?.shouldHideOnDetach()) {
      if (secondIsSidebar) {
        this.contentElement.insertBefore(this.#sidebarElement, this.#resizerElement);
      } else {
        this.contentElement.insertBefore(this.#sidebarElement, this.#mainElement);
      }
    } else {
      console.error('Could not swap split widget side. Both children widgets contain iframes.');
      this.#secondIsSidebar = !secondIsSidebar;
    }
  }

  resizerElement(): Element {
    return this.#resizerElement;
  }

  hideMain(animate?: boolean): void {
    this.#showOnly(this.#sidebarWidget, this.#mainWidget, this.#sidebarElement, this.#mainElement, animate);
    this.#updateShowMode(ShowMode.ONLY_SIDEBAR);
  }

  hideSidebar(animate?: boolean): void {
    this.#showOnly(this.#mainWidget, this.#sidebarWidget, this.#mainElement, this.#sidebarElement, animate);
    this.#updateShowMode(ShowMode.ONLY_MAIN);
  }

  setSidebarMinimized(minimized: boolean): void {
    this.#sidebarMinimized = minimized;
    this.invalidateConstraints();
  }

  isSidebarMinimized(): boolean {
    return this.#sidebarMinimized;
  }

  #showOnly(
      sideToShow: Widget|null,
      sideToHide: Widget|null,
      shadowToShow: Element,
      shadowToHide: Element,
      animate?: boolean,
      ): void {
    this.#cancelAnimation();

    function callback(this: SplitWidget): void {
      if (sideToShow) {
        // Make sure main is first in the children list.
        if (sideToShow === this.#mainWidget) {
          this.#mainWidget.show(this.element, this.#sidebarWidget ? this.#sidebarWidget.element : null);
        } else if (this.#sidebarWidget) {
          this.#sidebarWidget.show(this.element);
        }
      }
      if (sideToHide) {
        this.#detaching = true;
        sideToHide.detach();
        this.#detaching = false;
      }

      this.#resizerElement.classList.add('hidden');
      shadowToShow.classList.remove('hidden');
      shadowToShow.classList.add('maximized');
      shadowToHide.classList.add('hidden');
      shadowToHide.classList.remove('maximized');
      this.#removeAllLayoutProperties();
      this.doResize();
      this.showFinishedForTest();
    }

    if (animate) {
      this.#animate(true, callback.bind(this));
    } else {
      callback.call(this);
    }

    this.#sidebarSizeDIP = -1;
    this.setResizable(false);
  }

  protected showFinishedForTest(): void {
    // This method is sniffed in tests.
  }

  #removeAllLayoutProperties(): void {
    this.#sidebarElement.style.removeProperty('flexBasis');

    this.#mainElement.style.removeProperty('width');
    this.#mainElement.style.removeProperty('height');
    this.#sidebarElement.style.removeProperty('width');
    this.#sidebarElement.style.removeProperty('height');

    this.#resizerElement.style.removeProperty('left');
    this.#resizerElement.style.removeProperty('right');
    this.#resizerElement.style.removeProperty('top');
    this.#resizerElement.style.removeProperty('bottom');

    this.#resizerElement.style.removeProperty('margin-left');
    this.#resizerElement.style.removeProperty('margin-right');
    this.#resizerElement.style.removeProperty('margin-top');
    this.#resizerElement.style.removeProperty('margin-bottom');
  }

  showBoth(animate?: boolean): void {
    if (this.#showMode === ShowMode.BOTH) {
      animate = false;
    }

    this.#cancelAnimation();
    this.#mainElement.classList.remove('maximized', 'hidden');
    this.#sidebarElement.classList.remove('maximized', 'hidden');
    this.#resizerElement.classList.remove('hidden');
    this.setResizable(true);

    // Make sure main is the first in the children list.
    this.suspendInvalidations();
    if (this.#sidebarWidget) {
      this.#sidebarWidget.show(this.element);
    }
    if (this.#mainWidget) {
      this.#mainWidget.show(this.element, this.#sidebarWidget ? this.#sidebarWidget.element : null);
    }
    this.resumeInvalidations();
    // Order widgets in DOM properly.
    this.setSecondIsSidebar(this.#secondIsSidebar);

    this.#sidebarSizeDIP = -1;
    this.#updateShowMode(ShowMode.BOTH);
    this.#updateLayout(animate);
  }

  setResizable(resizable: boolean): void {
    this.#resizerWidget.setEnabled(resizable);
  }

  // Currently unused
  forceSetSidebarWidth(width: number): void {
    this.#defaultSidebarWidth = width;
    this.#savedSidebarSizeDIP = width;
    this.#updateLayout();
  }

  isResizable(): boolean {
    return this.#resizerWidget.isEnabled();
  }

  setSidebarSize(size: number): void {
    const sizeDIP = ZoomManager.instance().cssToDIP(size);
    this.#savedSidebarSizeDIP = sizeDIP;
    this.#saveSetting();
    this.#setSidebarSizeDIP(sizeDIP, false, true);
  }

  sidebarSize(): number {
    const sizeDIP = Math.max(0, this.#sidebarSizeDIP);
    return ZoomManager.instance().dipToCSS(sizeDIP);
  }

  totalSize(): number {
    const sizeDIP = Math.max(0, this.#totalSizeDIP());
    return ZoomManager.instance().dipToCSS(sizeDIP);
  }

  /**
   * Returns total size in DIP.
   */
  #totalSizeDIP(): number {
    if (!this.#totalSizeCSS) {
      const {width, height} = this.contentElement.getBoundingClientRect();
      this.#totalSizeCSS = this.#isVertical ? width : height;
      this.#totalSizeOtherDimensionCSS = this.#isVertical ? height : width;
    }
    return ZoomManager.instance().cssToDIP(this.#totalSizeCSS);
  }

  #updateShowMode(showMode: ShowMode): void {
    this.#showMode = showMode;
    this.#saveShowModeToSettings();
    this.#updateShowHideSidebarButton();
    this.dispatchEventToListeners(Events.SHOW_MODE_CHANGED, showMode);
    this.invalidateConstraints();
  }

  #setSidebarSizeDIP(sizeDIP: number, animate: boolean, userAction?: boolean): void {
    if (this.#showMode !== ShowMode.BOTH || !this.isShowing()) {
      return;
    }

    sizeDIP = this.#applyConstraints(sizeDIP, userAction);
    if (this.#sidebarSizeDIP === sizeDIP) {
      return;
    }

    if (!this.#resizerElementSize) {
      this.#resizerElementSize =
          this.#isVertical ? this.#resizerElement.offsetWidth : this.#resizerElement.offsetHeight;
    }

    // Invalidate layout below.

    this.#removeAllLayoutProperties();

    // this.#totalSizeDIP is available below since we successfully applied constraints.
    const sizeCSS = ZoomManager.instance().dipToCSS(sizeDIP);
    const sidebarSizeValue = sizeCSS + 'px';
    const mainSizeValue = (this.#totalSizeCSS - sizeCSS) + 'px';
    // With `box-sizing: border-box` on the sidebar (set in splitWidget.css),
    // flex-basis, width, and height all include the border, so the sidebar's
    // border does not steal space from the main pane.
    this.#sidebarElement.style.flexBasis = sidebarSizeValue;

    // Make both sides relayout boundaries.
    if (this.#isVertical) {
      this.#sidebarElement.style.width = sidebarSizeValue;
      this.#mainElement.style.width = mainSizeValue;
      this.#sidebarElement.style.height = this.#totalSizeOtherDimensionCSS + 'px';
      this.#mainElement.style.height = this.#totalSizeOtherDimensionCSS + 'px';
    } else {
      this.#sidebarElement.style.height = sidebarSizeValue;
      this.#mainElement.style.height = mainSizeValue;
      this.#sidebarElement.style.width = this.#totalSizeOtherDimensionCSS + 'px';
      this.#mainElement.style.width = this.#totalSizeOtherDimensionCSS + 'px';
    }

    // Position resizer.
    if (this.#isVertical) {
      if (this.#secondIsSidebar) {
        this.#resizerElement.style.right = sidebarSizeValue;
        this.#resizerElement.style.marginRight = -this.#resizerElementSize / 2 + 'px';
      } else {
        this.#resizerElement.style.left = sidebarSizeValue;
        this.#resizerElement.style.marginLeft = -this.#resizerElementSize / 2 + 'px';
      }
    } else if (this.#secondIsSidebar) {
      this.#resizerElement.style.bottom = sidebarSizeValue;
      this.#resizerElement.style.marginBottom = -this.#resizerElementSize / 2 + 'px';
    } else {
      this.#resizerElement.style.top = sidebarSizeValue;
      this.#resizerElement.style.marginTop = -this.#resizerElementSize / 2 + 'px';
    }

    this.#sidebarSizeDIP = sizeDIP;

    // Force layout.

    if (animate) {
      this.#animate(false);
    } else {
      // No need to recalculate this.sidebarSizeDIP and this.#totalSizeDIP again.
      this.doResize();
      this.dispatchEventToListeners(Events.SIDEBAR_SIZE_CHANGED, this.sidebarSize());
    }
  }

  #animate(reverse: boolean, callback?: (() => void)): void {
    const animationTime = 50;
    this.#animationCallback = callback || null;

    let animatedMarginPropertyName: string;
    if (this.#isVertical) {
      animatedMarginPropertyName = this.#secondIsSidebar ? 'margin-right' : 'margin-left';
    } else {
      animatedMarginPropertyName = this.#secondIsSidebar ? 'margin-bottom' : 'margin-top';
    }

    const marginFrom = reverse ? '0' : '-' + ZoomManager.instance().dipToCSS(this.#sidebarSizeDIP) + 'px';
    const marginTo = reverse ? '-' + ZoomManager.instance().dipToCSS(this.#sidebarSizeDIP) + 'px' : '0';

    // This order of things is important.
    // 1. Resize main element early and force layout.
    this.contentElement.style.setProperty(animatedMarginPropertyName, marginFrom);
    this.contentElement.style.setProperty('overflow', 'hidden');
    if (!reverse) {
      suppressUnused(this.#mainElement.offsetWidth);
      suppressUnused(this.#sidebarElement.offsetWidth);
    }

    // 2. Issue onresize to the sidebar element, its size won't change.
    if (!reverse && this.#sidebarWidget) {
      this.#sidebarWidget.doResize();
    }

    // 3. Configure and run animation
    this.contentElement.style.setProperty('transition', animatedMarginPropertyName + ' ' + animationTime + 'ms linear');

    const boundAnimationFrame = animationFrame.bind(this);
    let startTime: number|null = null;
    function animationFrame(this: SplitWidget): void {
      this.#animationFrameHandle = 0;

      if (!startTime) {
        // Kick animation on first frame.
        this.contentElement.style.setProperty(animatedMarginPropertyName, marginTo);
        startTime = window.performance.now();
      } else if (window.performance.now() < startTime + animationTime) {
        // Process regular animation frame.
        if (this.#mainWidget) {
          this.#mainWidget.doResize();
        }
      } else {
        // Complete animation.
        this.#cancelAnimation();
        if (this.#mainWidget) {
          this.#mainWidget.doResize();
        }
        this.dispatchEventToListeners(Events.SIDEBAR_SIZE_CHANGED, this.sidebarSize());
        return;
      }
      this.#animationFrameHandle = this.contentElement.window().requestAnimationFrame(boundAnimationFrame);
    }
    this.#animationFrameHandle = this.contentElement.window().requestAnimationFrame(boundAnimationFrame);
  }

  #cancelAnimation(): void {
    this.contentElement.style.removeProperty('margin-top');
    this.contentElement.style.removeProperty('margin-right');
    this.contentElement.style.removeProperty('margin-bottom');
    this.contentElement.style.removeProperty('margin-left');
    this.contentElement.style.removeProperty('transition');
    this.contentElement.style.removeProperty('overflow');

    if (this.#animationFrameHandle) {
      this.contentElement.window().cancelAnimationFrame(this.#animationFrameHandle);
      this.#animationFrameHandle = 0;
    }
    if (this.#animationCallback) {
      this.#animationCallback();
      this.#animationCallback = null;
    }
  }

  #applyConstraints(sidebarSize: number, userAction?: boolean): number {
    const totalSize = this.#totalSizeDIP();
    const zoomFactor = this.#constraintsInDip ? 1 : ZoomManager.instance().zoomFactor();

    let constraints: Geometry.Constraints =
        this.#sidebarWidget ? this.#sidebarWidget.constraints() : new Geometry.Constraints();
    let minSidebarSize: 20|number = this.isVertical() ? constraints.minimum.width : constraints.minimum.height;
    if (!minSidebarSize) {
      minSidebarSize = MinPadding;
    }
    minSidebarSize *= zoomFactor;
    if (this.#sidebarMinimized) {
      sidebarSize = minSidebarSize;
    }

    let preferredSidebarSize: 20|number =
        this.isVertical() ? constraints.preferred.width : constraints.preferred.height;
    if (!preferredSidebarSize) {
      preferredSidebarSize = MinPadding;
    }
    preferredSidebarSize *= zoomFactor;
    // Allow sidebar to be less than preferred by explicit user action.
    if (sidebarSize < preferredSidebarSize) {
      preferredSidebarSize = Math.max(sidebarSize, minSidebarSize);
    }
    preferredSidebarSize += zoomFactor;  // 1 css pixel for splitter border.

    constraints = this.#mainWidget ? this.#mainWidget.constraints() : new Geometry.Constraints();
    let minMainSize: 20|number = this.isVertical() ? constraints.minimum.width : constraints.minimum.height;
    if (!minMainSize) {
      minMainSize = MinPadding;
    }
    minMainSize *= zoomFactor;

    let preferredMainSize: 20|number = this.isVertical() ? constraints.preferred.width : constraints.preferred.height;
    if (!preferredMainSize) {
      preferredMainSize = MinPadding;
    }
    preferredMainSize *= zoomFactor;
    const savedMainSize = this.isVertical() ? this.#savedVerticalMainSize : this.#savedHorizontalMainSize;
    if (savedMainSize !== null) {
      preferredMainSize = Math.min(preferredMainSize, savedMainSize * zoomFactor);
    }
    if (userAction) {
      preferredMainSize = minMainSize;
    }

    // Enough space for preferred.
    const totalPreferred = preferredMainSize + preferredSidebarSize;
    if (totalPreferred <= totalSize) {
      return Platform.NumberUtilities.clamp(sidebarSize, preferredSidebarSize, totalSize - preferredMainSize);
    }

    // Enough space for minimum.
    if (minMainSize + minSidebarSize <= totalSize) {
      const delta = totalPreferred - totalSize;
      const sidebarDelta = delta * preferredSidebarSize / totalPreferred;
      sidebarSize = preferredSidebarSize - sidebarDelta;
      return Platform.NumberUtilities.clamp(sidebarSize, minSidebarSize, totalSize - minMainSize);
    }

    // Not enough space even for minimum sizes.
    return Math.max(0, totalSize - minMainSize);
  }

  override wasShown(): void {
    super.wasShown();
    this.#forceUpdateLayout();
    ZoomManager.instance().addEventListener(ZoomManagerEvents.ZOOM_CHANGED, this.onZoomChanged, this);
  }

  override willHide(): void {
    super.willHide();
    ZoomManager.instance().removeEventListener(ZoomManagerEvents.ZOOM_CHANGED, this.onZoomChanged, this);
  }

  override onResize(): void {
    this.#maybeAutoAdjustOrientation();
    this.#updateLayout();
  }

  override onLayout(): void {
    this.#updateLayout();
  }

  override calculateConstraints(): Geometry.Constraints {
    if (this.#showMode === ShowMode.ONLY_MAIN) {
      return this.#mainWidget ? this.#mainWidget.constraints() : new Geometry.Constraints();
    }
    if (this.#showMode === ShowMode.ONLY_SIDEBAR) {
      return this.#sidebarWidget ? this.#sidebarWidget.constraints() : new Geometry.Constraints();
    }

    let mainConstraints: Geometry.Constraints =
        this.#mainWidget ? this.#mainWidget.constraints() : new Geometry.Constraints();
    let sidebarConstraints: Geometry.Constraints =
        this.#sidebarWidget ? this.#sidebarWidget.constraints() : new Geometry.Constraints();
    const min = MinPadding;
    if (this.#isVertical) {
      mainConstraints = mainConstraints.widthToMax(min).addWidth(1);  // 1 for splitter
      sidebarConstraints = sidebarConstraints.widthToMax(min);
      return mainConstraints.addWidth(sidebarConstraints).heightToMax(sidebarConstraints);
    }
    mainConstraints = mainConstraints.heightToMax(min).addHeight(1);  // 1 for splitter
    sidebarConstraints = sidebarConstraints.heightToMax(min);
    return mainConstraints.widthToMax(sidebarConstraints).addHeight(sidebarConstraints);
  }

  #maybeAutoAdjustOrientation(): void {
    if (this.#autoAdjustOrientation) {
      const width = this.isVertical() ? this.#totalSizeCSS : this.#totalSizeOtherDimensionCSS;
      const height = this.isVertical() ? this.#totalSizeOtherDimensionCSS : this.#totalSizeCSS;
      if (width <= 600 && height >= 600) {
        this.setVertical(false);
      } else {
        this.setVertical(true);
      }
    }
  }

  #onResizeStart(): void {
    this.#resizeStartSizeDIP = this.#sidebarSizeDIP;
  }

  #onResizeUpdate(event: Common.EventTarget.EventTargetEvent<ResizeUpdatePositionEvent>): void {
    const offset = event.data.currentPosition - event.data.startPosition;
    const offsetDIP = ZoomManager.instance().cssToDIP(offset);
    const newSizeDIP =
        this.#secondIsSidebar ? this.#resizeStartSizeDIP - offsetDIP : this.#resizeStartSizeDIP + offsetDIP;
    const constrainedSizeDIP = this.#applyConstraints(newSizeDIP, true);
    this.#savedSidebarSizeDIP = constrainedSizeDIP;
    this.#saveSetting();
    this.#setSidebarSizeDIP(constrainedSizeDIP, false, true);
    if (this.isVertical()) {
      this.#savedVerticalMainSize = this.#totalSizeDIP() - this.#sidebarSizeDIP;
    } else {
      this.#savedHorizontalMainSize = this.#totalSizeDIP() - this.#sidebarSizeDIP;
    }
  }

  #onResizeEnd(): void {
    this.#resizeStartSizeDIP = 0;
  }

  hideDefaultResizer(noSplitter?: boolean): void {
    this.#resizerElement.classList.toggle('hidden', Boolean(noSplitter));
    this.uninstallResizer(this.#resizerElement);
    this.#sidebarElement.classList.toggle('no-default-splitter', Boolean(noSplitter));
  }

  installResizer(resizerElement: Element): void {
    this.#resizerWidget.addElement((resizerElement as HTMLElement));
  }

  uninstallResizer(resizerElement: Element): void {
    this.#resizerWidget.removeElement((resizerElement as HTMLElement));
  }

  toggleResizer(resizer: Element, on: boolean): void {
    if (on) {
      this.installResizer(resizer);
    } else {
      this.uninstallResizer(resizer);
    }
  }

  #settingForOrientation(): SettingForOrientation|null {
    const state = this.setting ? this.setting.get() : {};
    const orientationState = this.#isVertical ? state.vertical : state.horizontal;
    return orientationState ?? null;
  }

  #preferredSidebarSizeDIP(): number {
    let size: number = this.#savedSidebarSizeDIP;
    if (!size) {
      size = this.#isVertical ? this.#defaultSidebarWidth : this.#defaultSidebarHeight;
      // If we have default value in percents, calculate it on first use.
      if (0 < size && size < 1) {
        size *= this.#totalSizeDIP();
      }
    }
    return size;
  }

  #restoreSidebarSizeFromSettings(): void {
    const settingForOrientation = this.#settingForOrientation();
    this.#savedSidebarSizeDIP = settingForOrientation ? settingForOrientation.size : 0;
  }

  #restoreAndApplyShowModeFromSettings(): void {
    const orientationState = this.#settingForOrientation();
    this.#savedShowMode = orientationState?.showMode ? orientationState.showMode : this.#showMode;
    this.#showMode = this.#savedShowMode;

    switch (this.#savedShowMode) {
      case ShowMode.BOTH:
        this.showBoth();
        break;
      case ShowMode.ONLY_MAIN:
        this.hideSidebar();
        break;
      case ShowMode.ONLY_SIDEBAR:
        this.hideMain();
        break;
    }
  }

  #saveShowModeToSettings(): void {
    this.#savedShowMode = this.#showMode;
    this.#saveSetting();
  }

  #saveSetting(): void {
    if (!this.setting) {
      return;
    }
    const state = this.setting.get();
    const orientationState = (this.#isVertical ? state.vertical : state.horizontal) || {} as SettingForOrientation;

    orientationState.size = this.#savedSidebarSizeDIP;
    if (this.#shouldSaveShowMode) {
      orientationState.showMode = this.#savedShowMode;
    }

    if (this.#isVertical) {
      state.vertical = orientationState;
    } else {
      state.horizontal = orientationState;
    }
    this.setting.set(state);
  }

  #forceUpdateLayout(): void {
    // Force layout even if sidebar size does not change.
    this.#sidebarSizeDIP = -1;
    this.#updateLayout();
  }

  onZoomChanged(): void {
    this.#forceUpdateLayout();
  }

  createShowHideSidebarButton(
      showTitle: Common.UIString.LocalizedString, hideTitle: Common.UIString.LocalizedString,
      shownString: Common.UIString.LocalizedString, hiddenString: Common.UIString.LocalizedString,
      jslogContext?: string): ToolbarButton {
    this.#showSidebarButtonTitle = showTitle;
    this.#hideSidebarButtonTitle = hideTitle;
    this.#shownSidebarString = shownString;
    this.#hiddenSidebarString = hiddenString;
    this.#showHideSidebarButton = new ToolbarButton('', 'right-panel-open');
    this.#showHideSidebarButton.addEventListener(ToolbarButton.Events.CLICK, buttonClicked, this);
    if (jslogContext) {
      this.#showHideSidebarButton.element.setAttribute(
          'jslog', `${VisualLogging.toggleSubpane().track({click: true}).context(jslogContext)}`);
    }
    this.#updateShowHideSidebarButton();

    function buttonClicked(this: SplitWidget): void {
      this.toggleSidebar();
    }

    return this.#showHideSidebarButton;
  }

  /**
   * @returns true if this call makes the sidebar visible, and false otherwise.
   */
  toggleSidebar(): boolean {
    if (this.#showMode !== ShowMode.BOTH) {
      this.showBoth(true);
      ARIAUtils.LiveAnnouncer.alert(this.#shownSidebarString);
      return true;
    }
    this.hideSidebar(true);
    ARIAUtils.LiveAnnouncer.alert(this.#hiddenSidebarString);
    return false;
  }

  #updateShowHideSidebarButton(): void {
    if (!this.#showHideSidebarButton) {
      return;
    }
    const sidebarHidden = this.#showMode === ShowMode.ONLY_MAIN;
    let glyph = '';
    if (sidebarHidden) {
      glyph = this.isVertical() ? (this.isSidebarSecond() ? 'right-panel-open' : 'left-panel-open') :
                                  (this.isSidebarSecond() ? 'bottom-panel-open' : 'top-panel-open');
    } else {
      glyph = this.isVertical() ? (this.isSidebarSecond() ? 'right-panel-close' : 'left-panel-close') :
                                  (this.isSidebarSecond() ? 'bottom-panel-close' : 'top-panel-close');
    }
    this.#showHideSidebarButton.setGlyph(glyph);
    this.#showHideSidebarButton.setTitle(sidebarHidden ? this.#showSidebarButtonTitle : this.#hideSidebarButtonTitle);
  }
}

export class SplitWidgetElement extends WidgetElement<SplitWidget> {
  static readonly observedAttributes = ['direction', 'sidebar-position', 'sidebar-initial-size', 'sidebar-visibility'];

  constructor() {
    super();

    registerWidgetConfig(this, widgetConfig(element => {
                           const vertical = element.getAttribute('direction') === 'column';
                           const autoAdjustOrientation = element.getAttribute('direction') === 'auto';
                           const secondIsSidebar = element.getAttribute('sidebar-position') === 'second';
                           const settingName = element.getAttribute('name') ?? undefined;
                           const sidebarSize = parseInt(element.getAttribute('sidebar-initial-size') || '', 10);
                           const defaultSidebarWidth = !isNaN(sidebarSize) ? sidebarSize : undefined;
                           const defaultSidebarHeight = !isNaN(sidebarSize) ? sidebarSize : undefined;

                           const widget = new SplitWidget(
                               vertical, secondIsSidebar, settingName, defaultSidebarWidth, defaultSidebarHeight,
                               /* constraintsInDip=*/ false, element as SplitWidgetElement);

                           if (element.getAttribute('sidebar-initial-size') === 'minimized') {
                             widget.setSidebarMinimized(true);
                           }
                           if (autoAdjustOrientation) {
                             widget.setAutoAdjustOrientation(true);
                           }
                           const sidebarHidden = element.getAttribute('sidebar-visibility') === 'hidden';
                           if (sidebarHidden) {
                             widget.hideSidebar();
                           }
                           widget.addEventListener(Events.SHOW_MODE_CHANGED, () => {
                             element.dispatchEvent(new CustomEvent('change', {detail: widget.showMode()}));
                           });

                           return widget;
                         }));
  }

  attributeChangedCallback(name: string, _oldValue: string, newValue: string): void {
    const widget = Widget.get(this) as SplitWidget | null;
    if (!widget) {
      return;
    }
    if (name === 'direction') {
      widget.setVertical(newValue === 'column');
      widget.setAutoAdjustOrientation(newValue === 'auto');
    } else if (name === 'sidebar-position') {
      widget.setSecondIsSidebar(newValue === 'second');
    } else if (name === 'sidebar-visibility') {
      if (newValue === 'hidden') {
        widget.hideSidebar();
      } else {
        widget.showBoth();
      }
    }
  }
}

customElements.define('devtools-split-view', SplitWidgetElement);

export const enum ShowMode {
  BOTH = 'Both',
  ONLY_MAIN = 'OnlyMain',
  ONLY_SIDEBAR = 'OnlySidebar',
}

export const enum Events {
  SIDEBAR_SIZE_CHANGED = 'SidebarSizeChanged',
  SHOW_MODE_CHANGED = 'ShowModeChanged',
}

export interface EventTypes {
  [Events.SIDEBAR_SIZE_CHANGED]: number;
  [Events.SHOW_MODE_CHANGED]: string;
}

const MinPadding = 20;
export interface SettingForOrientation {
  showMode: ShowMode;
  size: number;
}

const suppressUnused = function(_value: unknown): void {};
