import React, { CSSProperties, Key } from 'react';
import { createPortal } from 'react-dom';
import { observer } from 'mobx-react';
import omit from 'lodash/omit';
import shallowEqual from 'shallowequal';
import noop from 'lodash/noop';
import isElement from 'lodash/isElement';
import { PopupManager } from 'choerodon-ui/shared';
import ViewComponent, { ViewComponentProps } from 'choerodon-ui/pro/lib/core/ViewComponent';
import autobind from 'choerodon-ui/pro/lib/_util/autobind';
import { findFocusableElements } from 'choerodon-ui/pro/lib/_util/focusable';
import { getDocument } from 'choerodon-ui/pro/lib/_util/DocumentUtils';
import Align from '../align';
import { getProPrefixCls } from '../configure/utils';
import Animate from '../animate';
import PopupInner from './PopupInner';

const childrenProps = { hidden: 'hidden' };

export interface PopupProps extends ViewComponentProps {
  align: object;
  onAlign?: (source: Node, align: object, target: HTMLElement, translate: { x: number; y: number }) => void;
  getRootDomNode?: () => Element | Text | null;
  getPopupContainer?: (triggerNode: Element) => HTMLElement | undefined | null;
  transitionName?: string;
  onAnimateAppear?: (key: Key | null) => void;
  onAnimateEnter?: (key: Key | null) => void;
  onAnimateLeave?: (key: Key | null) => void;
  onAnimateEnd?: (key: Key | null, exists: boolean) => void;
  getStyleFromAlign?: (target: HTMLElement, align: object) => object | undefined;
  getClassNameFromAlign?: (align: object) => string | undefined;
  getFocusableElements?: (elements: HTMLElement[]) => void;
  forceRender?: boolean;
}

function newPopupContainer() {
  const doc = getDocument(window);
  const popupContainer = doc.createElement('div');
  popupContainer.className = getProPrefixCls('popup-container');
  return popupContainer;
}

@observer
export default class Popup extends ViewComponent<PopupProps> {
  static displayName = 'Popup';

  static defaultProps = {
    suffixCls: 'popup',
    transitionName: 'zoom',
  };

  popupContainer?: HTMLDivElement;

  currentAlignClassName?: string;

  currentAlignStyle?: CSSProperties;

  align: Align | null;

  target?: HTMLElement;

  contentRendered = false;

  popupKey: string = PopupManager.getKey();

  size?: {
    width?: number;

    height?: number;
  };

  saveRef = align => (this.align = align);

  getOmitPropsKeys(): string[] {
    return super.getOmitPropsKeys().concat([
      'align',
      'transitionName',
      'getRootDomNode',
      'getPopupContainer',
      'getClassNameFromAlign',
      'getStyleFromAlign',
      'onAlign',
      'onAnimateAppear',
      'onAnimateEnter',
      'onAnimateLeave',
      'onAnimateEnd',
      'getFocusableElements',
      'forceRender',
    ]);
  }

  componentWillUnmount() {
    const { popupContainer } = this;
    if (popupContainer && popupContainer !== PopupManager.container && popupContainer.parentNode) {
      popupContainer.parentNode.removeChild(popupContainer);
    }
  }

  componentDidUpdate(): void {
    this.findFocusableElements();
  }

  componentDidMount(): void {
    super.componentDidMount();
    this.findFocusableElements();
  }

  findFocusableElements() {
    const { element } = this;
    const { getFocusableElements } = this.props;
    if (element && getFocusableElements) {
      const elements = findFocusableElements(element);
      getFocusableElements(elements && elements.filter(item => item.tabIndex !== -1).sort((e1, e2) => e1.tabIndex - e2.tabIndex));
    }
  }

  @autobind
  renderInner(innerRef) {
    const { children, getClassNameFromAlign = noop, align } = this.props;
    const className = this.getMergedClassNames(this.currentAlignClassName ||
      getClassNameFromAlign(align));
    return (
      <PopupInner
        {...omit(this.getMergedProps(), ['ref', 'className'])}
        className={className}
        innerRef={innerRef}
        onResize={this.handlePopupResize}
      >
        {children}
      </PopupInner>
    );
  }

  render() {
    const {
      hidden,
      align,
      transitionName,
      getRootDomNode,
      forceRender,
      onAnimateAppear = noop,
      onAnimateEnter = noop,
      onAnimateLeave = noop,
      onAnimateEnd = noop,
    } = this.props;
    if (!hidden || forceRender) {
      this.contentRendered = true;
    }
    const container = this.getContainer();
    return container && this.contentRendered ? createPortal(
      <Animate
        component=""
        exclusive
        transitionAppear
        transitionName={transitionName}
        hiddenProp="hidden"
        onAppear={onAnimateAppear}
        onEnter={onAnimateEnter}
        onLeave={onAnimateLeave}
        onEnd={onAnimateEnd}
      >
        <Align
          ref={this.saveRef}
          childrenRef={this.elementReference}
          key="align"
          childrenProps={childrenProps}
          align={align}
          onAlign={this.onAlign}
          target={getRootDomNode}
          hidden={hidden}
          monitorWindowResize
        >
          {this.renderInner}
        </Align>
      </Animate>,
      container,
      this.popupKey,
    ) : null;
  }

  getContainer(): HTMLDivElement | undefined {
    if (typeof window === 'undefined') {
      return undefined;
    }
    const { getPopupContainer, getRootDomNode = noop } = this.props;
    const globalContainer = PopupManager.container;
    if (getPopupContainer) {
      const container = this.popupContainer;
      if (container) {
        return container;
      }
    } else if (globalContainer) {
      return globalContainer;
    }
    if (getPopupContainer) {
      const mountNode = getPopupContainer(getRootDomNode());
      const popupContainer = newPopupContainer();
      const root = window.document.body;
      if (window === window.top && mountNode === root) {
        if (globalContainer) {
          this.popupContainer = globalContainer;
          return globalContainer;
        }
        PopupManager.container = popupContainer;
      }
      (mountNode && isElement(mountNode) ? mountNode : root).appendChild(popupContainer);
      this.popupContainer = popupContainer;
      return popupContainer;
    }
    // eslint-disable-next-line @typescript-eslint/no-use-before-define
    return getGlobalPopupContainer();
  }

  @autobind
  onAlign(source, align, target, translate) {
    const { getClassNameFromAlign = noop, getStyleFromAlign = noop, onAlign = noop } = this.props;
    const currentAlignClassName = getClassNameFromAlign(align);
    const differentTarget = target !== this.target;
    if (differentTarget || this.currentAlignClassName !== currentAlignClassName) {
      this.currentAlignClassName = currentAlignClassName;
      source.className = this.getMergedClassNames(currentAlignClassName);
    }
    const currentAlignStyle = getStyleFromAlign(target, align);
    if (differentTarget || !shallowEqual(this.currentAlignStyle, currentAlignStyle)) {
      this.currentAlignStyle = currentAlignStyle;
      Object.assign(source.style, currentAlignStyle);
    }
    onAlign(source, align, target, translate);
    this.target = source;
  }

  @autobind
  handlePopupResize(width, height) {
    if (width !== 0 && height !== 0) {
      const { width: oldWidth, height: oldHeight } = this.size || {};
      if (width !== oldWidth || height !== oldHeight) {
        this.size = { width, height };
        this.forceAlign();
      }
    }
  }

  forceAlign() {
    if (this.align) {
      this.align.forceAlign();
    }
  }
}

export function getGlobalPopupContainer() {
  if (PopupManager.container) {
    return PopupManager.container;
  }
  const popupContainer = newPopupContainer();
  const root = getDocument(window).body;
  root.appendChild(popupContainer);
  PopupManager.container = popupContainer;
  return popupContainer;
}