import {documentCursor} from 'app/client/lib/popupUtils';
import {hoverTooltip} from 'app/client/ui/tooltips';
import {isNarrowScreen, isNarrowScreenObs, theme, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {Disposable, dom, DomContents, DomElementArg,
        IDisposable, makeTestId, Observable, styled} from 'grainjs';

const POPUP_INITIAL_PADDING_PX = 16;
const POPUP_MIN_HEIGHT = 300;
const POPUP_MAX_HEIGHT = 711;
const POPUP_HEADER_HEIGHT = 30;

const testId = makeTestId('test-floating-popup-');

export interface PopupOptions {
  title?: () => DomContents;
  content?: () => DomContents;
  onClose?: () => void;
  closeButton?: boolean;
  closeButtonHover?: () => DomContents;
  autoHeight?: boolean;
  /** Defaults to false. */
  stopClickPropagationOnMove?: boolean;
  args?: DomElementArg[];
}

export class FloatingPopup extends Disposable {
  protected _isMinimized = Observable.create(this, false);
  private _isFinishingMove = false;
  private _popupElement: HTMLElement | null = null;
  private _popupMinimizeButtonElement: HTMLElement | null = null;

  private _startX: number;
  private _startY: number;
  private _initialTop: number;
  private _initialBottom: number;
  private _initialLeft: number;
  private _resize = false;
  private _cursorGrab: IDisposable|null = null;

  constructor(protected _options: PopupOptions = {}) {
    super();

    if (_options.stopClickPropagationOnMove){
      // weasel.js registers a 'click' listener that closes any open popups that
      // are outside the click target. We capture the click event here, stopping
      // propagation in a few scenarios where closing popups is undesirable.
      window.addEventListener('click', (ev) => {
        if (this._isFinishingMove) {
          ev.stopPropagation();
          this._isFinishingMove = false;
          return;
        }

        if (this._popupMinimizeButtonElement?.contains(ev.target as Node)) {
          ev.stopPropagation();
          this._minimizeOrMaximize();
          return;
        }
      }, {capture: true});
    }

    this._handleMouseDown = this._handleMouseDown.bind(this);
    this._handleMouseMove = this._handleMouseMove.bind(this);
    this._handleMouseUp = this._handleMouseUp.bind(this);
    this._handleTouchStart = this._handleTouchStart.bind(this);
    this._handleTouchMove = this._handleTouchMove.bind(this);
    this._handleTouchEnd = this._handleTouchEnd.bind(this);
    this._handleWindowResize = this._handleWindowResize.bind(this);

    this.autoDispose(isNarrowScreenObs().addListener(() => this._repositionPopup()));

    this.onDispose(() => {
      this._closePopup();
      this._cursorGrab?.dispose();
    });
  }

  public showPopup() {
    this._popupElement = this._buildPopup();
    document.body.appendChild(this._popupElement);
    const topPaddingPx = getTopPopupPaddingPx();
    const initialLeft = document.body.offsetWidth - this._popupElement.offsetWidth - POPUP_INITIAL_PADDING_PX;
    const initialTop = document.body.offsetHeight - this._popupElement.offsetHeight - topPaddingPx;
    this._popupElement.style.left = `${initialLeft}px`;
    this._popupElement.style.top = `${initialTop}px`;
  }

  protected _closePopup() {
    if (!this._popupElement) { return; }
    document.body.removeChild(this._popupElement);
    dom.domDispose(this._popupElement);
    this._popupElement = null;
  }

  protected _buildTitle(): DomContents {
    return this._options.title?.() ?? null;
  }

  protected _buildContent(): DomContents {
    return this._options.content?.() ?? null;
  }

  protected _buildArgs(): any {
    return this._options.args ?? [];
  }

  private _rememberPosition() {
    this._initialLeft = this._popupElement!.offsetLeft;
    this._initialTop = this._popupElement!.offsetTop;
    this._initialBottom = this._popupElement!.offsetTop + this._popupElement!.offsetHeight;
  }

  private _handleMouseDown(ev: MouseEvent) {
    if (ev.button !== 0) { return; } // Only handle left-click.
    this._startX = ev.clientX;
    this._startY = ev.clientY;
    this._rememberPosition();
    document.addEventListener('mousemove', this._handleMouseMove);
    document.addEventListener('mouseup', this._handleMouseUp);
    this._forceCursor();
  }

  private _handleTouchStart(ev: TouchEvent) {
    this._startX = ev.touches[0].clientX;
    this._startY = ev.touches[0].clientY;
    this._rememberPosition();
    document.addEventListener('touchmove', this._handleTouchMove);
    document.addEventListener('touchend', this._handleTouchEnd);

    this._resize = false;
    this._forceCursor();
  }

  private _handleTouchMove({touches}: TouchEvent) {
    this._handleMouseMove(touches[0]);
  }

  private _handleMouseMove({clientX, clientY}: MouseEvent | Touch) {
    if (this._resize) {
      this._handleResize(clientY);
    } else {
      this._handleMove(clientX, clientY);
    }
  }

  private _handleResize(clientY: number) {
    const deltaY = clientY - this._startY;
    if (this._resize && !isNarrowScreen()) {
      // First calculate the boundaries for the new top.

      // First just how much we can resize the popup.
      let minTop = this._initialBottom - POPUP_MAX_HEIGHT;
      let maxTop = this._initialBottom - POPUP_MIN_HEIGHT;

      // Now how far we can move top (leave at least some padding for mobile).
      minTop = Math.max(minTop, getTopPopupPaddingPx());
      // And bottom (we want the header to be visible)
      maxTop = Math.min(document.body.offsetHeight - POPUP_HEADER_HEIGHT - 2, maxTop);

      // Now get new top from those boundaries.
      const newTop = Math.max(minTop, Math.min(maxTop, this._initialTop + deltaY));
      // And calculate the new height.
      const newHeight = this._initialBottom - newTop;
      this._popupElement!.style.top = `${newTop}px`;
      this._popupElement!.style.setProperty('--height', `${newHeight}px`);
      return;
    }
  }

  private _handleMove(clientX: number, clientY: number) {
    // Last change in position (from last move).
    const deltaX = clientX - this._startX;
    const deltaY = clientY - this._startY;

    // Available space where we can put the popup (anchored at top left corner).
    const viewPort = {
      right: document.body.offsetWidth,
      bottom: document.body.offsetHeight,
      top: getTopPopupPaddingPx(),
      left: 0,
    };

    // Allow some extra space, where we can still move the popup outside the viewport.
    viewPort.right += this._popupElement!.offsetWidth - (POPUP_HEADER_HEIGHT + 2) * 4;
    viewPort.left -= this._popupElement!.offsetWidth - (POPUP_HEADER_HEIGHT + 2) * 4;
    viewPort.bottom += this._popupElement!.offsetHeight - POPUP_HEADER_HEIGHT - 2; // 2px border top

    let newLeft = this._initialLeft + deltaX;
    let newTop = this._initialTop + deltaY;
    const newRight = (val?: number) => {
      if (val !== undefined) { newLeft = val - this._popupElement!.offsetWidth; }
      return newLeft + this._popupElement!.offsetWidth;
    };
    const newBottom = (val?: number) => {
      if (val !== undefined) { newTop = val - this._popupElement!.offsetHeight; }
      return newTop + this._popupElement!.offsetHeight;
    };

    // Calculate new position in the padding area.
    if (newLeft < viewPort.left) { newLeft = viewPort.left; }
    if (newRight() > viewPort.right) { newRight(viewPort.right); }
    if (newTop  < viewPort.top) { newTop = viewPort.top; }
    if (newBottom() > viewPort.bottom) { newBottom(viewPort.bottom); }

    this._popupElement!.style.left = `${newLeft}px`;
    this._popupElement!.style.top = `${newTop}px`;
  }

  private _handleMouseUp() {
    this._isFinishingMove = true;
    document.removeEventListener('mousemove', this._handleMouseMove);
    document.removeEventListener('mouseup', this._handleMouseUp);
    document.body.removeEventListener('mouseleave', this._handleMouseUp);
    this._handleMouseEnd();
  }

  private _handleTouchEnd() {
    document.removeEventListener('touchmove', this._handleTouchMove);
    document.removeEventListener('touchend', this._handleTouchEnd);
    document.body.removeEventListener('touchcancel', this._handleTouchEnd);
    this._handleMouseEnd();
  }

  private _handleMouseEnd() {
    this._resize = false;
    this._cursorGrab?.dispose();
  }

  private _handleWindowResize() {
    this._repositionPopup();
  }

  private _repositionPopup() {
    let newLeft = this._popupElement!.offsetLeft;
    let newTop = this._popupElement!.offsetTop;

    const topPaddingPx = getTopPopupPaddingPx();
    if (newLeft - POPUP_INITIAL_PADDING_PX < 0) { newLeft = POPUP_INITIAL_PADDING_PX; }
    if (newTop - topPaddingPx < 0) { newTop = topPaddingPx; }
    if (newLeft + POPUP_INITIAL_PADDING_PX > document.body.offsetWidth - this._popupElement!.offsetWidth) {
      newLeft = document.body.offsetWidth - this._popupElement!.offsetWidth - POPUP_INITIAL_PADDING_PX;
    }
    if (newTop + topPaddingPx > document.body.offsetHeight - this._popupElement!.offsetHeight) {
      newTop = document.body.offsetHeight - this._popupElement!.offsetHeight - topPaddingPx;
    }

    this._popupElement!.style.left = `${newLeft}px`;
    this._popupElement!.style.top = `${newTop}px`;
  }

  private _minimizeOrMaximize() {
    this._isMinimized.set(!this._isMinimized.get());
    this._repositionPopup();
  }

  private _buildPopup() {
    const body = cssPopup(
      {tabIndex: '-1'},
      cssPopup.cls('-auto', this._options.autoHeight ?? false),
      cssPopupHeader(
        cssBottomHandle(testId('move-handle')),
        dom.maybe(use => !use(this._isMinimized), () => {
          return cssResizeTopLayer(
            cssTopHandle(testId('resize-handle')),
            dom.on('mousedown', () => this._resize = true),
            dom.on('dblclick', (e) => {
              e.stopImmediatePropagation();
              this._popupElement?.style.setProperty('--height', `${POPUP_MAX_HEIGHT}px`);
              this._repositionPopup();
            })
          );
        }),
        dom.domComputed(this._isMinimized, isMinimized => {
          return [
            // Copy buttons on the left side of the header, to automatically
            // center the title.
            cssPopupButtons(
              cssPopupHeaderButton(
                icon('Maximize')
              ),
              !this._options.closeButton ? null : cssPopupHeaderButton(
                icon('CrossBig'),
              ),
              dom.style('visibility', 'hidden'),
            ),
            cssPopupTitle(
              cssPopupTitleText(this._buildTitle()),
              testId('title'),
            ),
            cssPopupButtons(
              this._popupMinimizeButtonElement = cssPopupHeaderButton(
                isMinimized ? icon('Maximize'): icon('Minimize'),
                hoverTooltip(isMinimized ? 'Maximize' : 'Minimize', {key: 'docTutorialTooltip'}),
                dom.on('click', () => this._minimizeOrMaximize()),
                testId('minimize-maximize'),
              ),
              !this._options.closeButton ? null : cssPopupHeaderButton(
                icon('CrossBig'),
                dom.on('click', () => {
                  this._options.onClose?.() ?? this._closePopup();
                }),
                testId('close'),
                this._options.closeButtonHover && hoverTooltip(this._options.closeButtonHover())
              ),
              // Disable dragging when a button in the header is clicked.
              dom.on('mousedown', ev => ev.stopPropagation()),
              dom.on('touchstart', ev => ev.stopPropagation()),
            )
          ];
        }),
        dom.on('mousedown', this._handleMouseDown),
        dom.on('touchstart', this._handleTouchStart),
        dom.on('dblclick', () => this._minimizeOrMaximize()),
        testId('header'),
      ),
      cssPopupContent(
        this._buildContent(),
        cssPopupContent.cls('-minimized', this._isMinimized),
      ),
      () => { window.addEventListener('resize', this._handleWindowResize); },
      dom.onDispose(() => {
        document.removeEventListener('mousemove', this._handleMouseMove);
        document.removeEventListener('mouseup', this._handleMouseUp);
        document.removeEventListener('touchmove', this._handleTouchMove);
        document.removeEventListener('touchend', this._handleTouchEnd);
        window.removeEventListener('resize', this._handleWindowResize);
      }),
      cssPopup.cls('-minimized', this._isMinimized),
      cssPopup.cls('-mobile', isNarrowScreenObs()),
      testId('window'),
      this._buildArgs()
    );

    // For auto-height popups, we need to reposition the popup when the content changes.
    // It is important for auto-grow and to prevent popup from going off-screen.
    if (this._options.autoHeight) {
      const observer = new MutationObserver(() => {
        this._repositionPopup();
      });
      observer.observe(body, {childList: true, subtree: true});
      dom.update(body,
        dom.onDispose(() => observer.disconnect())
      );
    }

    return body;
  }

  private _forceCursor() {
    this._cursorGrab?.dispose();
    const type = this._resize ? 'ns-resize' : 'grabbing';
    this._cursorGrab = documentCursor(type);
  }
}


function getTopPopupPaddingPx(): number {
  // On mobile, we need additional padding to avoid blocking the top and bottom bars.
  return POPUP_INITIAL_PADDING_PX + (isNarrowScreen() ? 50 : 0);
}

const POPUP_HEIGHT = `min(var(--height), calc(100% - (2 * ${POPUP_INITIAL_PADDING_PX}px)))`;
const POPUP_HEIGHT_MOBILE = `min(var(--height), calc(100% - (2 * ${POPUP_INITIAL_PADDING_PX}px) - (2 * 50px)))`;
const POPUP_WIDTH = `min(436px, calc(100% - (2 * ${POPUP_INITIAL_PADDING_PX}px)))`;

const cssPopup = styled('div.floating-popup', `
  position: fixed;
  display: flex;
  flex-direction: column;
  border: 2px solid ${theme.accentBorder};
  border-radius: 5px;
  z-index: ${vars.floatingPopupZIndex};
  --height: ${POPUP_MAX_HEIGHT}px;
  height: ${POPUP_HEIGHT};
  width: ${POPUP_WIDTH};
  min-height: ${POPUP_MIN_HEIGHT}px;
  background-color: ${theme.popupBg};
  box-shadow: 0 2px 18px 0 ${theme.popupInnerShadow}, 0 0 1px 0 ${theme.popupOuterShadow};
  outline: unset;

  &-mobile {
    height: ${POPUP_HEIGHT_MOBILE};
  }

  &-minimized {
    max-width: 225px;
    height: unset;
    min-height: unset;
  }

  &-minimized:not(&-mobile) {
    max-height: ${POPUP_HEIGHT};
  }

  &-minimized&-mobile {
    max-height: ${POPUP_HEIGHT_MOBILE};
  }

  &-auto {
    height: auto;
    max-height: ${POPUP_HEIGHT};
    min-height: unset;
  }

  &-auto&-mobile {
    max-height: ${POPUP_HEIGHT_MOBILE};
  }
`);

const cssPopupHeader = styled('div', `
  color: ${theme.tutorialsPopupHeaderFg};
  --icon-color: ${theme.tutorialsPopupHeaderFg};
  background-color: ${theme.accentBorder};
  align-items: center;
  flex-shrink: 0;
  cursor: grab;
  padding-left: 4px;
  padding-right: 4px;
  height: ${POPUP_HEADER_HEIGHT}px;
  user-select: none;
  display: flex;
  justify-content: space-between;
  position: relative;
  isolation: isolate;
  &:active {
    cursor: grabbing;
  }
`);

const cssPopupButtons = styled('div', `
  display: flex;
  column-gap: 8px;
  align-items: center;
`);

const cssPopupTitle = styled('div', `
  display: flex;
  justify-content: center;
  align-items: center;
  font-weight: 600;
  overflow: hidden;
`);

const cssPopupTitleText = styled('div', `
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
`);

export const cssPopupBody = styled('div', `
  flex-grow: 1;
  padding: 24px;
  overflow: auto;
`);

const cssPopupHeaderButton = styled('div', `
  padding: 4px;
  border-radius: 4px;
  cursor: pointer;
  z-index: 1000;

  &:hover {
    background-color: ${theme.hover};
  }
`);

const cssResizeTopLayer = styled('div', `
  position: absolute;
  top: -6px;
  left: 0;
  right: 0;
  bottom: 28px;
  z-index: 500;
  cursor: ns-resize;
`);

const cssTopHandle = styled('div', `
  position: absolute;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
  user-select: none;
  visibility: hidden;
`);

const cssBottomHandle = styled(cssTopHandle, `
  top: unset;
  bottom: 0;
  left: 0;
`);

const cssPopupContent = styled('div', `
  display: flex;
  flex-direction: column;
  flex-grow: 1;
  overflow: hidden;

  &-minimized {
    display: none;
  }
`);
