// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { find } from '@lumino/algorithm';

import { MimeData } from '@lumino/coreutils';

import { IDisposable } from '@lumino/disposable';

import { ElementExt, Platform } from '@lumino/domutils';

import { Drag } from '@lumino/dragdrop';

import { ConflatableMessage, Message, MessageLoop } from '@lumino/messaging';

import { AttachedProperty } from '@lumino/properties';

import { ISignal, Signal } from '@lumino/signaling';

import { DockLayout } from './docklayout';

import { TabBar } from './tabbar';

import { Widget } from './widget';

/**
 * A widget which provides a flexible docking area for widgets.
 */
export class DockPanel extends Widget {
  /**
   * Construct a new dock panel.
   *
   * @param options - The options for initializing the panel.
   */
  constructor(options: DockPanel.IOptions = {}) {
    super();
    this.addClass('lm-DockPanel');
    this._document = options.document || document;
    this._mode = options.mode || 'multiple-document';
    this._renderer = options.renderer || DockPanel.defaultRenderer;
    this._edges = options.edges || Private.DEFAULT_EDGES;
    if (options.tabsMovable !== undefined) {
      this._tabsMovable = options.tabsMovable;
    }
    if (options.tabsConstrained !== undefined) {
      this._tabsConstrained = options.tabsConstrained;
    }
    if (options.addButtonEnabled !== undefined) {
      this._addButtonEnabled = options.addButtonEnabled;
    }

    // Toggle the CSS mode attribute.
    this.dataset['mode'] = this._mode;

    // Create the delegate renderer for the layout.
    let renderer: DockPanel.IRenderer = {
      createTabBar: () => this._createTabBar(),
      createHandle: () => this._createHandle()
    };

    // Set up the dock layout for the panel.
    this.layout = new DockLayout({
      document: this._document,
      renderer,
      spacing: options.spacing,
      hiddenMode: options.hiddenMode
    });

    // Set up the overlay drop indicator.
    this.overlay = options.overlay || new DockPanel.Overlay();
    this.node.appendChild(this.overlay.node);
  }

  /**
   * Dispose of the resources held by the panel.
   */
  dispose(): void {
    // Ensure the mouse is released.
    this._releaseMouse();

    // Hide the overlay.
    this.overlay.hide(0);

    // Cancel a drag if one is in progress.
    if (this._drag) {
      this._drag.dispose();
    }

    // Dispose of the base class.
    super.dispose();
  }

  /**
   * The method for hiding widgets.
   */
  get hiddenMode(): Widget.HiddenMode {
    return (this.layout as DockLayout).hiddenMode;
  }

  /**
   * Set the method for hiding widgets.
   */
  set hiddenMode(v: Widget.HiddenMode) {
    (this.layout as DockLayout).hiddenMode = v;
  }

  /**
   * A signal emitted when the layout configuration is modified.
   *
   * #### Notes
   * This signal is emitted whenever the current layout configuration
   * may have changed.
   *
   * This signal is emitted asynchronously in a collapsed fashion, so
   * that multiple synchronous modifications results in only a single
   * emit of the signal.
   */
  get layoutModified(): ISignal<this, void> {
    return this._layoutModified;
  }

  /**
   * A signal emitted when the add button on a tab bar is clicked.
   *
   */
  get addRequested(): ISignal<this, TabBar<Widget>> {
    return this._addRequested;
  }

  /**
   * The overlay used by the dock panel.
   */
  readonly overlay: DockPanel.IOverlay;

  /**
   * The renderer used by the dock panel.
   */
  get renderer(): DockPanel.IRenderer {
    return (this.layout as DockLayout).renderer;
  }

  /**
   * Get the spacing between the widgets.
   */
  get spacing(): number {
    return (this.layout as DockLayout).spacing;
  }

  /**
   * Set the spacing between the widgets.
   */
  set spacing(value: number) {
    (this.layout as DockLayout).spacing = value;
  }

  /**
   * Get the mode for the dock panel.
   */
  get mode(): DockPanel.Mode {
    return this._mode;
  }

  /**
   * Set the mode for the dock panel.
   *
   * #### Notes
   * Changing the mode is a destructive operation with respect to the
   * panel's layout configuration. If layout state must be preserved,
   * save the current layout config before changing the mode.
   */
  set mode(value: DockPanel.Mode) {
    // Bail early if the mode does not change.
    if (this._mode === value) {
      return;
    }

    // Update the internal mode.
    this._mode = value;

    // Toggle the CSS mode attribute.
    this.dataset['mode'] = value;

    // Get the layout for the panel.
    let layout = this.layout as DockLayout;

    // Configure the layout for the specified mode.
    switch (value) {
      case 'multiple-document':
        for (const tabBar of layout.tabBars()) {
          tabBar.show();
        }
        break;
      case 'single-document':
        layout.restoreLayout(Private.createSingleDocumentConfig(this));
        break;
      default:
        throw 'unreachable';
    }

    // Schedule an emit of the layout modified signal.
    MessageLoop.postMessage(this, Private.LayoutModified);
  }

  /**
   * Whether the tabs can be dragged / moved at runtime.
   */
  get tabsMovable(): boolean {
    return this._tabsMovable;
  }

  /**
   * Enable / Disable draggable / movable tabs.
   */
  set tabsMovable(value: boolean) {
    this._tabsMovable = value;
    for (const tabBar of this.tabBars()) {
      tabBar.tabsMovable = value;
    }
  }

  /**
   * Whether the tabs are constrained to their source dock panel
   */
  get tabsConstrained(): boolean {
    return this._tabsConstrained;
  }

  /**
   * Constrain/Allow tabs to be dragged outside of this dock panel
   */
  set tabsConstrained(value: boolean) {
    this._tabsConstrained = value;
  }

  /**
   * Whether the add buttons for each tab bar are enabled.
   */
  get addButtonEnabled(): boolean {
    return this._addButtonEnabled;
  }

  /**
   * Set whether the add buttons for each tab bar are enabled.
   */
  set addButtonEnabled(value: boolean) {
    this._addButtonEnabled = value;
    for (const tabBar of this.tabBars()) {
      tabBar.addButtonEnabled = value;
    }
  }

  /**
   * Whether the dock panel is empty.
   */
  get isEmpty(): boolean {
    return (this.layout as DockLayout).isEmpty;
  }

  /**
   * Create an iterator over the user widgets in the panel.
   *
   * @returns A new iterator over the user widgets in the panel.
   *
   * #### Notes
   * This iterator does not include the generated tab bars.
   */
  *widgets(): IterableIterator<Widget> {
    yield* (this.layout as DockLayout).widgets();
  }

  /**
   * Create an iterator over the selected widgets in the panel.
   *
   * @returns A new iterator over the selected user widgets.
   *
   * #### Notes
   * This iterator yields the widgets corresponding to the current tab
   * of each tab bar in the panel.
   */
  *selectedWidgets(): IterableIterator<Widget> {
    yield* (this.layout as DockLayout).selectedWidgets();
  }

  /**
   * Create an iterator over the tab bars in the panel.
   *
   * @returns A new iterator over the tab bars in the panel.
   *
   * #### Notes
   * This iterator does not include the user widgets.
   */
  *tabBars(): IterableIterator<TabBar<Widget>> {
    yield* (this.layout as DockLayout).tabBars();
  }

  /**
   * Create an iterator over the handles in the panel.
   *
   * @returns A new iterator over the handles in the panel.
   */
  *handles(): IterableIterator<HTMLDivElement> {
    yield* (this.layout as DockLayout).handles();
  }

  /**
   * Select a specific widget in the dock panel.
   *
   * @param widget - The widget of interest.
   *
   * #### Notes
   * This will make the widget the current widget in its tab area.
   */
  selectWidget(widget: Widget): void {
    // Find the tab bar which contains the widget.
    let tabBar = find(this.tabBars(), bar => {
      return bar.titles.indexOf(widget.title) !== -1;
    });

    // Throw an error if no tab bar is found.
    if (!tabBar) {
      throw new Error('Widget is not contained in the dock panel.');
    }

    // Ensure the widget is the current widget.
    tabBar.currentTitle = widget.title;
  }

  /**
   * Activate a specified widget in the dock panel.
   *
   * @param widget - The widget of interest.
   *
   * #### Notes
   * This will select and activate the given widget.
   */
  activateWidget(widget: Widget): void {
    this.selectWidget(widget);
    widget.activate();
  }

  /**
   * Save the current layout configuration of the dock panel.
   *
   * @returns A new config object for the current layout state.
   *
   * #### Notes
   * The return value can be provided to the `restoreLayout` method
   * in order to restore the layout to its current configuration.
   */
  saveLayout(): DockPanel.ILayoutConfig {
    return (this.layout as DockLayout).saveLayout();
  }

  /**
   * Restore the layout to a previously saved configuration.
   *
   * @param config - The layout configuration to restore.
   *
   * #### Notes
   * Widgets which currently belong to the layout but which are not
   * contained in the config will be unparented.
   *
   * The dock panel automatically reverts to `'multiple-document'`
   * mode when a layout config is restored.
   */
  restoreLayout(config: DockPanel.ILayoutConfig): void {
    // Reset the mode.
    this._mode = 'multiple-document';

    // Restore the layout.
    (this.layout as DockLayout).restoreLayout(config);

    // Flush the message loop on IE and Edge to prevent flicker.
    if (Platform.IS_EDGE || Platform.IS_IE) {
      MessageLoop.flush();
    }

    // Schedule an emit of the layout modified signal.
    MessageLoop.postMessage(this, Private.LayoutModified);
  }

  /**
   * Add a widget to the dock panel.
   *
   * @param widget - The widget to add to the dock panel.
   *
   * @param options - The additional options for adding the widget.
   *
   * #### Notes
   * If the panel is in single document mode, the options are ignored
   * and the widget is always added as tab in the hidden tab bar.
   */
  addWidget(widget: Widget, options: DockPanel.IAddOptions = {}): void {
    // Add the widget to the layout.
    if (this._mode === 'single-document') {
      (this.layout as DockLayout).addWidget(widget);
    } else {
      (this.layout as DockLayout).addWidget(widget, options);
    }

    // Schedule an emit of the layout modified signal.
    MessageLoop.postMessage(this, Private.LayoutModified);
  }

  /**
   * Process a message sent to the widget.
   *
   * @param msg - The message sent to the widget.
   */
  processMessage(msg: Message): void {
    if (msg.type === 'layout-modified') {
      this._layoutModified.emit(undefined);
    } else {
      super.processMessage(msg);
    }
  }

  /**
   * Handle the DOM events for the dock panel.
   *
   * @param event - The DOM event sent to the panel.
   *
   * #### Notes
   * This method implements the DOM `EventListener` interface and is
   * called in response to events on the panel's DOM node. It should
   * not be called directly by user code.
   */
  handleEvent(event: Event): void {
    switch (event.type) {
      case 'lm-dragenter':
        this._evtDragEnter(event as Drag.Event);
        break;
      case 'lm-dragleave':
        this._evtDragLeave(event as Drag.Event);
        break;
      case 'lm-dragover':
        this._evtDragOver(event as Drag.Event);
        break;
      case 'lm-drop':
        this._evtDrop(event as Drag.Event);
        break;
      case 'pointerdown':
        this._evtPointerDown(event as PointerEvent);
        break;
      case 'pointermove':
        this._evtPointerMove(event as PointerEvent);
        break;
      case 'pointerup':
        this._evtPointerUp(event as PointerEvent);
        break;
      case 'keydown':
        this._evtKeyDown(event as KeyboardEvent);
        break;
      case 'contextmenu':
        event.preventDefault();
        event.stopPropagation();
        break;
    }
  }

  /**
   * A message handler invoked on a `'before-attach'` message.
   */
  protected onBeforeAttach(msg: Message): void {
    this.node.addEventListener('lm-dragenter', this);
    this.node.addEventListener('lm-dragleave', this);
    this.node.addEventListener('lm-dragover', this);
    this.node.addEventListener('lm-drop', this);
    this.node.addEventListener('pointerdown', this);
  }

  /**
   * A message handler invoked on an `'after-detach'` message.
   */
  protected onAfterDetach(msg: Message): void {
    this.node.removeEventListener('lm-dragenter', this);
    this.node.removeEventListener('lm-dragleave', this);
    this.node.removeEventListener('lm-dragover', this);
    this.node.removeEventListener('lm-drop', this);
    this.node.removeEventListener('pointerdown', this);
    this._releaseMouse();
  }

  /**
   * A message handler invoked on a `'child-added'` message.
   */
  protected onChildAdded(msg: Widget.ChildMessage): void {
    // Ignore the generated tab bars.
    if (Private.isGeneratedTabBarProperty.get(msg.child)) {
      return;
    }

    // Add the widget class to the child.
    msg.child.addClass('lm-DockPanel-widget');
  }

  /**
   * A message handler invoked on a `'child-removed'` message.
   */
  protected onChildRemoved(msg: Widget.ChildMessage): void {
    // Ignore the generated tab bars.
    if (Private.isGeneratedTabBarProperty.get(msg.child)) {
      return;
    }

    // Remove the widget class from the child.
    msg.child.removeClass('lm-DockPanel-widget');

    // Schedule an emit of the layout modified signal.
    MessageLoop.postMessage(this, Private.LayoutModified);
  }

  /**
   * Handle the `'lm-dragenter'` event for the dock panel.
   */
  private _evtDragEnter(event: Drag.Event): void {
    // If the factory mime type is present, mark the event as
    // handled in order to get the rest of the drag events.
    if (event.mimeData.hasData('application/vnd.lumino.widget-factory')) {
      event.preventDefault();
      event.stopPropagation();
    }
  }

  /**
   * Handle the `'lm-dragleave'` event for the dock panel.
   */
  private _evtDragLeave(event: Drag.Event): void {
    // Mark the event as handled.
    event.preventDefault();

    if (this._tabsConstrained && event.source !== this) return;

    event.stopPropagation();

    // The new target might be a descendant, so we might still handle the drop.
    // Hide asynchronously so that if a lm-dragover event bubbles up to us, the
    // hide is cancelled by the lm-dragover handler's show overlay logic.
    this.overlay.hide(1);
  }

  /**
   * Handle the `'lm-dragover'` event for the dock panel.
   */
  private _evtDragOver(event: Drag.Event): void {
    // Mark the event as handled.
    event.preventDefault();

    // Show the drop indicator overlay and update the drop
    // action based on the drop target zone under the mouse.
    if (
      (this._tabsConstrained && event.source !== this) ||
      this._showOverlay(event.clientX, event.clientY) === 'invalid'
    ) {
      event.dropAction = 'none';
    } else {
      event.stopPropagation();
      event.dropAction = event.proposedAction;
    }
  }

  /**
   * Handle the `'lm-drop'` event for the dock panel.
   */
  private _evtDrop(event: Drag.Event): void {
    // Mark the event as handled.
    event.preventDefault();

    // Hide the drop indicator overlay.
    this.overlay.hide(0);

    // Bail if the proposed action is to do nothing.
    if (event.proposedAction === 'none') {
      event.dropAction = 'none';
      return;
    }

    // Find the drop target under the mouse.
    let { clientX, clientY } = event;
    let { zone, target } = Private.findDropTarget(
      this,
      clientX,
      clientY,
      this._edges
    );

    // Bail if the drop zone is invalid.
    if (
      (this._tabsConstrained && event.source !== this) ||
      zone === 'invalid'
    ) {
      event.dropAction = 'none';
      return;
    }

    // Bail if the factory mime type has invalid data.
    let mimeData = event.mimeData;
    let factory = mimeData.getData('application/vnd.lumino.widget-factory');
    if (typeof factory !== 'function') {
      event.dropAction = 'none';
      return;
    }

    // Bail if the factory does not produce a widget.
    let widget = factory();
    if (!(widget instanceof Widget)) {
      event.dropAction = 'none';
      return;
    }

    // Bail if the widget is an ancestor of the dock panel.
    if (widget.contains(this)) {
      event.dropAction = 'none';
      return;
    }

    // Find the reference widget for the drop target.
    let ref = target ? Private.getDropRef(target.tabBar) : null;

    // Add the widget according to the indicated drop zone.
    switch (zone) {
      case 'root-all':
        this.addWidget(widget);
        break;
      case 'root-top':
        this.addWidget(widget, { mode: 'split-top' });
        break;
      case 'root-left':
        this.addWidget(widget, { mode: 'split-left' });
        break;
      case 'root-right':
        this.addWidget(widget, { mode: 'split-right' });
        break;
      case 'root-bottom':
        this.addWidget(widget, { mode: 'split-bottom' });
        break;
      case 'widget-all':
        this.addWidget(widget, { mode: 'tab-after', ref });
        break;
      case 'widget-top':
        this.addWidget(widget, { mode: 'split-top', ref });
        break;
      case 'widget-left':
        this.addWidget(widget, { mode: 'split-left', ref });
        break;
      case 'widget-right':
        this.addWidget(widget, { mode: 'split-right', ref });
        break;
      case 'widget-bottom':
        this.addWidget(widget, { mode: 'split-bottom', ref });
        break;
      case 'widget-tab':
        this.addWidget(widget, { mode: 'tab-after', ref });
        break;
      default:
        throw 'unreachable';
    }

    // Accept the proposed drop action.
    event.dropAction = event.proposedAction;

    // Stop propagation if we have not bailed so far.
    event.stopPropagation();

    // Activate the dropped widget.
    this.activateWidget(widget);
  }

  /**
   * Handle the `'keydown'` event for the dock panel.
   */
  private _evtKeyDown(event: KeyboardEvent): void {
    // Stop input events during drag.
    event.preventDefault();
    event.stopPropagation();

    // Release the mouse if `Escape` is pressed.
    if (event.keyCode === 27) {
      // Finalize the mouse release.
      this._releaseMouse();

      // Schedule an emit of the layout modified signal.
      MessageLoop.postMessage(this, Private.LayoutModified);
    }
  }

  /**
   * Handle the `'pointerdown'` event for the dock panel.
   */
  private _evtPointerDown(event: PointerEvent): void {
    // Do nothing if the left mouse button is not pressed.
    if (event.button !== 0) {
      return;
    }

    // Find the handle which contains the mouse target, if any.
    let layout = this.layout as DockLayout;
    let target = event.target as HTMLElement;
    let handle = find(layout.handles(), handle => handle.contains(target));
    if (!handle) {
      return;
    }

    // Stop the event when a handle is pressed.
    event.preventDefault();
    event.stopPropagation();

    // Add the extra document listeners.
    this._document.addEventListener('keydown', this, true);
    this._document.addEventListener('pointerup', this, true);
    this._document.addEventListener('pointermove', this, true);
    this._document.addEventListener('contextmenu', this, true);

    // Compute the offset deltas for the handle press.
    let rect = handle.getBoundingClientRect();
    let deltaX = event.clientX - rect.left;
    let deltaY = event.clientY - rect.top;

    // Override the cursor and store the press data.
    let style = window.getComputedStyle(handle);
    let override = Drag.overrideCursor(style.cursor!, this._document);
    this._pressData = { handle, deltaX, deltaY, override };
  }

  /**
   * Handle the `'pointermove'` event for the dock panel.
   */
  private _evtPointerMove(event: PointerEvent): void {
    // Bail early if no drag is in progress.
    if (!this._pressData) {
      return;
    }

    // Stop the event when dragging a handle.
    event.preventDefault();
    event.stopPropagation();

    // Compute the desired offset position for the handle.
    let rect = this.node.getBoundingClientRect();
    let xPos = event.clientX - rect.left - this._pressData.deltaX;
    let yPos = event.clientY - rect.top - this._pressData.deltaY;

    // Set the handle as close to the desired position as possible.
    let layout = this.layout as DockLayout;
    layout.moveHandle(this._pressData.handle, xPos, yPos);
  }

  /**
   * Handle the `'pointerup'` event for the dock panel.
   */
  private _evtPointerUp(event: PointerEvent): void {
    // Do nothing if the left mouse button is not released.
    if (event.button !== 0) {
      return;
    }

    // Stop the event when releasing a handle.
    event.preventDefault();
    event.stopPropagation();

    // Finalize the mouse release.
    this._releaseMouse();

    // Schedule an emit of the layout modified signal.
    MessageLoop.postMessage(this, Private.LayoutModified);
  }

  /**
   * Release the mouse grab for the dock panel.
   */
  private _releaseMouse(): void {
    // Bail early if no drag is in progress.
    if (!this._pressData) {
      return;
    }

    // Clear the override cursor.
    this._pressData.override.dispose();
    this._pressData = null;

    // Remove the extra document listeners.
    this._document.removeEventListener('keydown', this, true);
    this._document.removeEventListener('pointerup', this, true);
    this._document.removeEventListener('pointermove', this, true);
    this._document.removeEventListener('contextmenu', this, true);
  }

  /**
   * Show the overlay indicator at the given client position.
   *
   * Returns the drop zone at the specified client position.
   *
   * #### Notes
   * If the position is not over a valid zone, the overlay is hidden.
   */
  private _showOverlay(clientX: number, clientY: number): Private.DropZone {
    // Find the dock target for the given client position.
    let { zone, target } = Private.findDropTarget(
      this,
      clientX,
      clientY,
      this._edges
    );

    // If the drop zone is invalid, hide the overlay and bail.
    if (zone === 'invalid') {
      this.overlay.hide(100);
      return zone;
    }

    // Setup the variables needed to compute the overlay geometry.
    let top: number;
    let left: number;
    let right: number;
    let bottom: number;
    let box = ElementExt.boxSizing(this.node); // TODO cache this?
    let rect = this.node.getBoundingClientRect();

    // Compute the overlay geometry based on the dock zone.
    switch (zone) {
      case 'root-all':
        top = box.paddingTop;
        left = box.paddingLeft;
        right = box.paddingRight;
        bottom = box.paddingBottom;
        break;
      case 'root-top':
        top = box.paddingTop;
        left = box.paddingLeft;
        right = box.paddingRight;
        bottom = rect.height * Private.GOLDEN_RATIO;
        break;
      case 'root-left':
        top = box.paddingTop;
        left = box.paddingLeft;
        right = rect.width * Private.GOLDEN_RATIO;
        bottom = box.paddingBottom;
        break;
      case 'root-right':
        top = box.paddingTop;
        left = rect.width * Private.GOLDEN_RATIO;
        right = box.paddingRight;
        bottom = box.paddingBottom;
        break;
      case 'root-bottom':
        top = rect.height * Private.GOLDEN_RATIO;
        left = box.paddingLeft;
        right = box.paddingRight;
        bottom = box.paddingBottom;
        break;
      case 'widget-all':
        top = target!.top;
        left = target!.left;
        right = target!.right;
        bottom = target!.bottom;
        break;
      case 'widget-top':
        top = target!.top;
        left = target!.left;
        right = target!.right;
        bottom = target!.bottom + target!.height / 2;
        break;
      case 'widget-left':
        top = target!.top;
        left = target!.left;
        right = target!.right + target!.width / 2;
        bottom = target!.bottom;
        break;
      case 'widget-right':
        top = target!.top;
        left = target!.left + target!.width / 2;
        right = target!.right;
        bottom = target!.bottom;
        break;
      case 'widget-bottom':
        top = target!.top + target!.height / 2;
        left = target!.left;
        right = target!.right;
        bottom = target!.bottom;
        break;
      case 'widget-tab': {
        const tabHeight = target!.tabBar.node.getBoundingClientRect().height;
        top = target!.top;
        left = target!.left;
        right = target!.right;
        bottom = target!.bottom + target!.height - tabHeight;
        break;
      }
      default:
        throw 'unreachable';
    }

    // Show the overlay with the computed geometry.
    this.overlay.show({ top, left, right, bottom });

    // Finally, return the computed drop zone.
    return zone;
  }

  /**
   * Create a new tab bar for use by the panel.
   */
  private _createTabBar(): TabBar<Widget> {
    // Create the tab bar.
    let tabBar = this._renderer.createTabBar(this._document);

    // Set the generated tab bar property for the tab bar.
    Private.isGeneratedTabBarProperty.set(tabBar, true);

    // Hide the tab bar when in single document mode.
    if (this._mode === 'single-document') {
      tabBar.hide();
    }

    // Enforce necessary tab bar behavior.
    // TODO do we really want to enforce *all* of these?
    tabBar.tabsMovable = this._tabsMovable;
    tabBar.allowDeselect = false;
    tabBar.addButtonEnabled = this._addButtonEnabled;
    tabBar.removeBehavior = 'select-previous-tab';
    tabBar.insertBehavior = 'select-tab-if-needed';

    // Connect the signal handlers for the tab bar.
    tabBar.tabMoved.connect(this._onTabMoved, this);
    tabBar.currentChanged.connect(this._onCurrentChanged, this);
    tabBar.tabCloseRequested.connect(this._onTabCloseRequested, this);
    tabBar.tabDetachRequested.connect(this._onTabDetachRequested, this);
    tabBar.tabActivateRequested.connect(this._onTabActivateRequested, this);
    tabBar.addRequested.connect(this._onTabAddRequested, this);

    // Return the initialized tab bar.
    return tabBar;
  }

  /**
   * Create a new handle for use by the panel.
   */
  private _createHandle(): HTMLDivElement {
    return this._renderer.createHandle();
  }

  /**
   * Handle the `tabMoved` signal from a tab bar.
   */
  private _onTabMoved(): void {
    MessageLoop.postMessage(this, Private.LayoutModified);
  }

  /**
   * Handle the `currentChanged` signal from a tab bar.
   */
  private _onCurrentChanged(
    sender: TabBar<Widget>,
    args: TabBar.ICurrentChangedArgs<Widget>
  ): void {
    // Extract the previous and current title from the args.
    let { previousTitle, currentTitle } = args;

    // Hide the previous widget.
    if (previousTitle) {
      previousTitle.owner.hide();
    }

    // Show the current widget.
    if (currentTitle) {
      currentTitle.owner.show();
    }

    // Flush the message loop on IE and Edge to prevent flicker.
    if (Platform.IS_EDGE || Platform.IS_IE) {
      MessageLoop.flush();
    }

    // Schedule an emit of the layout modified signal.
    MessageLoop.postMessage(this, Private.LayoutModified);
  }

  /**
   * Handle the `addRequested` signal from a tab bar.
   */
  private _onTabAddRequested(sender: TabBar<Widget>): void {
    this._addRequested.emit(sender);
  }

  /**
   * Handle the `tabActivateRequested` signal from a tab bar.
   */
  private _onTabActivateRequested(
    sender: TabBar<Widget>,
    args: TabBar.ITabActivateRequestedArgs<Widget>
  ): void {
    args.title.owner.activate();
  }

  /**
   * Handle the `tabCloseRequested` signal from a tab bar.
   */
  private _onTabCloseRequested(
    sender: TabBar<Widget>,
    args: TabBar.ITabCloseRequestedArgs<Widget>
  ): void {
    args.title.owner.close();
  }

  /**
   * Handle the `tabDetachRequested` signal from a tab bar.
   */
  private _onTabDetachRequested(
    sender: TabBar<Widget>,
    args: TabBar.ITabDetachRequestedArgs<Widget>
  ): void {
    // Do nothing if a drag is already in progress.
    if (this._drag) {
      return;
    }

    // Release the tab bar's hold on the mouse.
    sender.releaseMouse();

    // Extract the data from the args.
    let { title, tab, clientX, clientY, offset } = args;

    // Setup the mime data for the drag operation.
    let mimeData = new MimeData();
    let factory = () => title.owner;
    mimeData.setData('application/vnd.lumino.widget-factory', factory);

    // Create the drag image for the drag operation.
    let dragImage = tab.cloneNode(true) as HTMLElement;
    if (offset) {
      dragImage.style.top = `-${offset.y}px`;
      dragImage.style.left = `-${offset.x}px`;
    }

    // Create the drag object to manage the drag-drop operation.
    this._drag = new Drag({
      document: this._document,
      mimeData,
      dragImage,
      proposedAction: 'move',
      supportedActions: 'move',
      source: this
    });

    // Hide the tab node in the original tab.
    tab.classList.add('lm-mod-hidden');
    let cleanup = () => {
      this._drag = null;
      tab.classList.remove('lm-mod-hidden');
    };

    // Start the drag operation and cleanup when done.
    this._drag.start(clientX, clientY).then(cleanup);
  }

  private _edges: DockPanel.IEdges;
  private _document: Document | ShadowRoot;
  private _mode: DockPanel.Mode;
  private _drag: Drag | null = null;
  private _renderer: DockPanel.IRenderer;
  private _tabsMovable: boolean = true;
  private _tabsConstrained: boolean = false;
  private _addButtonEnabled: boolean = false;
  private _pressData: Private.IPressData | null = null;
  private _layoutModified = new Signal<this, void>(this);

  private _addRequested = new Signal<this, TabBar<Widget>>(this);
}

/**
 * The namespace for the `DockPanel` class statics.
 */
export namespace DockPanel {
  /**
   * An options object for creating a dock panel.
   */
  export interface IOptions {
    /**
     * The document to use with the dock panel.
     *
     * The default is the global `document` instance.
     */

    document?: Document | ShadowRoot;
    /**
     * The overlay to use with the dock panel.
     *
     * The default is a new `Overlay` instance.
     */
    overlay?: IOverlay;

    /**
     * The renderer to use for the dock panel.
     *
     * The default is a shared renderer instance.
     */
    renderer?: IRenderer;

    /**
     * The spacing between the items in the panel.
     *
     * The default is `4`.
     */
    spacing?: number;

    /**
     * The mode for the dock panel.
     *
     * The default is `'multiple-document'`.
     */
    mode?: DockPanel.Mode;

    /**
     * The sizes of the edge drop zones, in pixels.
     * If not given, default values will be used.
     */
    edges?: IEdges;

    /**
     * The method for hiding widgets.
     *
     * The default is `Widget.HiddenMode.Display`.
     */
    hiddenMode?: Widget.HiddenMode;

    /**
     * Allow tabs to be draggable / movable by user.
     *
     * The default is `'true'`.
     */
    tabsMovable?: boolean;

    /**
     * Constrain tabs to this dock panel
     *
     * The default is `'false'`.
     */
    tabsConstrained?: boolean;

    /**
     * Enable add buttons in each of the dock panel's tab bars.
     *
     * The default is `'false'`.
     */
    addButtonEnabled?: boolean;
  }

  /**
   * The sizes of the edge drop zones, in pixels.
   */
  export interface IEdges {
    /**
     * The size of the top edge drop zone.
     */
    top: number;

    /**
     * The size of the right edge drop zone.
     */
    right: number;

    /**
     * The size of the bottom edge drop zone.
     */
    bottom: number;

    /**
     * The size of the left edge drop zone.
     */
    left: number;
  }

  /**
   * A type alias for the supported dock panel modes.
   */
  export type Mode =
    | /**
     * The single document mode.
     *
     * In this mode, only a single widget is visible at a time, and that
     * widget fills the available layout space. No tab bars are visible.
     */
    'single-document'

    /**
     * The multiple document mode.
     *
     * In this mode, multiple documents are displayed in separate tab
     * areas, and those areas can be individually resized by the user.
     */
    | 'multiple-document';

  /**
   * A type alias for a layout configuration object.
   */
  export type ILayoutConfig = DockLayout.ILayoutConfig;

  /**
   * A type alias for the supported insertion modes.
   */
  export type InsertMode = DockLayout.InsertMode;

  /**
   * A type alias for the add widget options.
   */
  export type IAddOptions = DockLayout.IAddOptions;

  /**
   * An object which holds the geometry for overlay positioning.
   */
  export interface IOverlayGeometry {
    /**
     * The distance between the overlay and parent top edges.
     */
    top: number;

    /**
     * The distance between the overlay and parent left edges.
     */
    left: number;

    /**
     * The distance between the overlay and parent right edges.
     */
    right: number;

    /**
     * The distance between the overlay and parent bottom edges.
     */
    bottom: number;
  }

  /**
   * An object which manages the overlay node for a dock panel.
   */
  export interface IOverlay {
    /**
     * The DOM node for the overlay.
     */
    readonly node: HTMLDivElement;

    /**
     * Show the overlay using the given overlay geometry.
     *
     * @param geo - The desired geometry for the overlay.
     *
     * #### Notes
     * The given geometry values assume the node will use absolute
     * positioning.
     *
     * This is called on every mouse move event during a drag in order
     * to update the position of the overlay. It should be efficient.
     */
    show(geo: IOverlayGeometry): void;

    /**
     * Hide the overlay node.
     *
     * @param delay - The delay (in ms) before hiding the overlay.
     *   A delay value <= 0 should hide the overlay immediately.
     *
     * #### Notes
     * This is called whenever the overlay node should been hidden.
     */
    hide(delay: number): void;
  }

  /**
   * A concrete implementation of `IOverlay`.
   *
   * This is the default overlay implementation for a dock panel.
   */
  export class Overlay implements IOverlay {
    /**
     * Construct a new overlay.
     */
    constructor() {
      this.node = document.createElement('div');
      this.node.classList.add('lm-DockPanel-overlay');
      this.node.classList.add('lm-mod-hidden');
      this.node.style.position = 'absolute';
      this.node.style.contain = 'strict';
    }

    /**
     * The DOM node for the overlay.
     */
    readonly node: HTMLDivElement;

    /**
     * Show the overlay using the given overlay geometry.
     *
     * @param geo - The desired geometry for the overlay.
     */
    show(geo: IOverlayGeometry): void {
      // Update the position of the overlay.
      let style = this.node.style;
      style.top = `${geo.top}px`;
      style.left = `${geo.left}px`;
      style.right = `${geo.right}px`;
      style.bottom = `${geo.bottom}px`;

      // Clear any pending hide timer.
      clearTimeout(this._timer);
      this._timer = -1;

      // If the overlay is already visible, we're done.
      if (!this._hidden) {
        return;
      }

      // Clear the hidden flag.
      this._hidden = false;

      // Finally, show the overlay.
      this.node.classList.remove('lm-mod-hidden');
    }

    /**
     * Hide the overlay node.
     *
     * @param delay - The delay (in ms) before hiding the overlay.
     *   A delay value <= 0 will hide the overlay immediately.
     */
    hide(delay: number): void {
      // Do nothing if the overlay is already hidden.
      if (this._hidden) {
        return;
      }

      // Hide immediately if the delay is <= 0.
      if (delay <= 0) {
        clearTimeout(this._timer);
        this._timer = -1;
        this._hidden = true;
        this.node.classList.add('lm-mod-hidden');
        return;
      }

      // Do nothing if a hide is already pending.
      if (this._timer !== -1) {
        return;
      }

      // Otherwise setup the hide timer.
      this._timer = window.setTimeout(() => {
        this._timer = -1;
        this._hidden = true;
        this.node.classList.add('lm-mod-hidden');
      }, delay);
    }

    private _timer = -1;
    private _hidden = true;
  }

  /**
   * A type alias for a dock panel renderer;
   */
  export type IRenderer = DockLayout.IRenderer;

  /**
   * The default implementation of `IRenderer`.
   */
  export class Renderer implements IRenderer {
    /**
     * Create a new tab bar for use with a dock panel.
     *
     * @returns A new tab bar for a dock panel.
     */
    createTabBar(document?: Document | ShadowRoot): TabBar<Widget> {
      let bar = new TabBar<Widget>({ document });
      bar.addClass('lm-DockPanel-tabBar');
      return bar;
    }

    /**
     * Create a new handle node for use with a dock panel.
     *
     * @returns A new handle node for a dock panel.
     */
    createHandle(): HTMLDivElement {
      let handle = document.createElement('div');
      handle.className = 'lm-DockPanel-handle';
      return handle;
    }
  }

  /**
   * The default `Renderer` instance.
   */
  export const defaultRenderer = new Renderer();
}

/**
 * The namespace for the module implementation details.
 */
namespace Private {
  /**
   * A fraction used for sizing root panels; ~= `1 / golden_ratio`.
   */
  export const GOLDEN_RATIO = 0.618;

  /**
   * The default sizes for the edge drop zones, in pixels.
   */
  export const DEFAULT_EDGES = {
    /**
     * The size of the top edge dock zone for the root panel, in pixels.
     * This is different from the others to distinguish between the top
     * tab bar and the top root zone.
     */
    top: 12,

    /**
     * The size of the edge dock zone for the root panel, in pixels.
     */
    right: 40,

    /**
     * The size of the edge dock zone for the root panel, in pixels.
     */
    bottom: 40,

    /**
     * The size of the edge dock zone for the root panel, in pixels.
     */
    left: 40
  };

  /**
   * A singleton `'layout-modified'` conflatable message.
   */
  export const LayoutModified = new ConflatableMessage('layout-modified');

  /**
   * An object which holds mouse press data.
   */
  export interface IPressData {
    /**
     * The handle which was pressed.
     */
    handle: HTMLDivElement;

    /**
     * The X offset of the press in handle coordinates.
     */
    deltaX: number;

    /**
     * The Y offset of the press in handle coordinates.
     */
    deltaY: number;

    /**
     * The disposable which will clear the override cursor.
     */
    override: IDisposable;
  }

  /**
   * A type alias for a drop zone.
   */
  export type DropZone =
    | /**
     * An invalid drop zone.
     */
    'invalid'

    /**
     * The entirety of the root dock area.
     */
    | 'root-all'

    /**
     * The top portion of the root dock area.
     */
    | 'root-top'

    /**
     * The left portion of the root dock area.
     */
    | 'root-left'

    /**
     * The right portion of the root dock area.
     */
    | 'root-right'

    /**
     * The bottom portion of the root dock area.
     */
    | 'root-bottom'

    /**
     * The entirety of a tabbed widget area.
     */
    | 'widget-all'

    /**
     * The top portion of tabbed widget area.
     */
    | 'widget-top'

    /**
     * The left portion of tabbed widget area.
     */
    | 'widget-left'

    /**
     * The right portion of tabbed widget area.
     */
    | 'widget-right'

    /**
     * The bottom portion of tabbed widget area.
     */
    | 'widget-bottom'

    /**
     * The the bar of a tabbed widget area.
     */
    | 'widget-tab';

  /**
   * An object which holds the drop target zone and widget.
   */
  export interface IDropTarget {
    /**
     * The semantic zone for the mouse position.
     */
    zone: DropZone;

    /**
     * The tab area geometry for the drop zone, or `null`.
     */
    target: DockLayout.ITabAreaGeometry | null;
  }

  /**
   * An attached property used to track generated tab bars.
   */
  export const isGeneratedTabBarProperty = new AttachedProperty<
    Widget,
    boolean
  >({
    name: 'isGeneratedTabBar',
    create: () => false
  });

  /**
   * Create a single document config for the widgets in a dock panel.
   */
  export function createSingleDocumentConfig(
    panel: DockPanel
  ): DockPanel.ILayoutConfig {
    // Return an empty config if the panel is empty.
    if (panel.isEmpty) {
      return { main: null };
    }

    // Get a flat array of the widgets in the panel.
    let widgets = Array.from(panel.widgets());

    // Get the first selected widget in the panel.
    let selected = panel.selectedWidgets().next().value;

    // Compute the current index for the new config.
    let currentIndex = selected ? widgets.indexOf(selected) : -1;

    // Return the single document config.
    return { main: { type: 'tab-area', widgets, currentIndex } };
  }

  /**
   * Find the drop target at the given client position.
   */
  export function findDropTarget(
    panel: DockPanel,
    clientX: number,
    clientY: number,
    edges: DockPanel.IEdges
  ): IDropTarget {
    // Bail if the mouse is not over the dock panel.
    if (!ElementExt.hitTest(panel.node, clientX, clientY)) {
      return { zone: 'invalid', target: null };
    }

    // Look up the layout for the panel.
    let layout = panel.layout as DockLayout;

    // If the layout is empty, indicate the entire root drop zone.
    if (layout.isEmpty) {
      return { zone: 'root-all', target: null };
    }

    // Test the edge zones when in multiple document mode.
    if (panel.mode === 'multiple-document') {
      // Get the client rect for the dock panel.
      let panelRect = panel.node.getBoundingClientRect();

      // Compute the distance to each edge of the panel.
      let pl = clientX - panelRect.left + 1;
      let pt = clientY - panelRect.top + 1;
      let pr = panelRect.right - clientX;
      let pb = panelRect.bottom - clientY;

      // Find the minimum distance to an edge.
      let pd = Math.min(pt, pr, pb, pl);

      // Return a root zone if the mouse is within an edge.
      switch (pd) {
        case pt:
          if (pt < edges.top) {
            return { zone: 'root-top', target: null };
          }
          break;
        case pr:
          if (pr < edges.right) {
            return { zone: 'root-right', target: null };
          }
          break;
        case pb:
          if (pb < edges.bottom) {
            return { zone: 'root-bottom', target: null };
          }
          break;
        case pl:
          if (pl < edges.left) {
            return { zone: 'root-left', target: null };
          }
          break;
        default:
          throw 'unreachable';
      }
    }

    // Hit test the dock layout at the given client position.
    let target = layout.hitTestTabAreas(clientX, clientY);

    // Bail if no target area was found.
    if (!target) {
      return { zone: 'invalid', target: null };
    }

    // Return the whole tab area when in single document mode.
    if (panel.mode === 'single-document') {
      return { zone: 'widget-all', target };
    }

    // Compute the distance to each edge of the tab area.
    let al = target.x - target.left + 1;
    let at = target.y - target.top + 1;
    let ar = target.left + target.width - target.x;
    let ab = target.top + target.height - target.y;

    const tabHeight = target.tabBar.node.getBoundingClientRect().height;
    if (at < tabHeight) {
      return { zone: 'widget-tab', target };
    }

    // Get the X and Y edge sizes for the area.
    let rx = Math.round(target.width / 3);
    let ry = Math.round(target.height / 3);

    // If the mouse is not within an edge, indicate the entire area.
    if (al > rx && ar > rx && at > ry && ab > ry) {
      return { zone: 'widget-all', target };
    }

    // Scale the distances by the slenderness ratio.
    al /= rx;
    at /= ry;
    ar /= rx;
    ab /= ry;

    // Find the minimum distance to the area edge.
    let ad = Math.min(al, at, ar, ab);

    // Find the widget zone for the area edge.
    let zone: DropZone;
    switch (ad) {
      case al:
        zone = 'widget-left';
        break;
      case at:
        zone = 'widget-top';
        break;
      case ar:
        zone = 'widget-right';
        break;
      case ab:
        zone = 'widget-bottom';
        break;
      default:
        throw 'unreachable';
    }

    // Return the final drop target.
    return { zone, target };
  }

  /**
   * Get the drop reference widget for a tab bar.
   */
  export function getDropRef(tabBar: TabBar<Widget>): Widget | null {
    if (tabBar.titles.length === 0) {
      return null;
    }
    if (tabBar.currentTitle) {
      return tabBar.currentTitle.owner;
    }
    return tabBar.titles[tabBar.titles.length - 1].owner;
  }
}
