import React, { Children } from 'react';
import { findDOMNode } from 'react-dom';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import shallowElementEquals from 'shallow-element-equals';
import { dom, log, obj, events, env } from '../util';
import LockRow from './lock/row';
import LockBody from './lock/body';
import LockHeader from './lock/header';
import LockWrapper from './fixed/wrapper';
import { statics } from './util';

const { ieVersion } = env;
export default function lock(BaseComponent) {
  /** Table */
  class LockTable extends React.Component {
    static LockRow = LockRow;

    static LockBody = LockBody;

    static LockHeader = LockHeader;

    static propTypes = {
      scrollToCol: PropTypes.number,
      /**
             * 指定滚动到某一行，仅在`useVirtual`的时候生效
             */
      scrollToRow: PropTypes.number,
      ...BaseComponent.propTypes,
    };

    static defaultProps = {
      ...BaseComponent.defaultProps,
    };

    static childContextTypes = {
      getTableInstance: PropTypes.func,
      getLockNode: PropTypes.func,
      onLockBodyScroll: PropTypes.func,
      onRowMouseEnter: PropTypes.func,
      onRowMouseLeave: PropTypes.func,
    };

    constructor(props, context) {
      super(props, context);
      this.lockLeftChildren = [];
      this.lockRightChildren = [];
    }

    getChildContext() {
      return {
        getTableInstance: this.getTableInstance,
        getLockNode: this.getNode,
        onLockBodyScroll: this.onLockBodyScroll,
        onRowMouseEnter: this.onRowMouseEnter,
        onRowMouseLeave: this.onRowMouseLeave,
      };
    }

    componentDidMount() {
      events.on(window, 'resize', this.adjustSize);

      this.scroll();
      this.adjustSize();
    }

    shouldComponentUpdate(nextProps, nextState, nextContext) {
      if (nextProps.pure) {
        const isEqual = shallowElementEquals(nextProps, this.props);
        return !(
          isEqual && obj.shallowEqual(nextContext, this.context)
        );
      }

      return true;
    }

    componentWillUpdate() {
      this._isLock = false;
    }

    componentDidUpdate() {
      this.adjustSize();
    }

    componentWillUnmount() {
      events.off(window, 'resize', this.adjustSize);
    }

    normalizeChildrenState(props) {
      let { children } = props;
      children = this.normalizeChildren(children);
      const splitChildren = this.splitFromNormalizeChildren(children);
      const { lockLeftChildren, lockRightChildren } = splitChildren;
      return {
        lockLeftChildren,
        lockRightChildren,
        children: this.mergeFromSplitLockChildren(splitChildren),
      };
    }

    // 将React结构化数据提取props转换成数组
    normalizeChildren(children) {
      let isLock = false;
      const getChildren = children => {
        const ret = [];
        Children.forEach(children, child => {
          if (child) {
            const props = { ...child.props };
            if ([true, 'left', 'right'].indexOf(props.lock) > -1) {
              isLock = true;
              if (!('width' in props)) {
                log.warning(
                  `Should config width for lock column named [ ${
                    props.dataIndex
                  } ].`,
                );
              }
            }
            ret.push(props);
            if (child.props.children) {
              props.children = getChildren(child.props.children);
            }
          }
        });
        return ret;
      };
      const ret = getChildren(children);
      ret.forEach(child => {
        // 为自定义的列特殊处理
        if (child.__normalized && isLock) {
          // users can set lock type for column selection now, so its origin lock type comes first
          child.lock = child.lock || 'left';
          delete child.__normalized;
        }
      });
      this._isLock = isLock;
      return ret;
    }

    // 从数组中分离出lock的列和正常的列
    splitFromNormalizeChildren(children) {
      const originChildren = deepCopy(children);
      const lockLeftChildren = deepCopy(children);
      const lockRightChildren = deepCopy(children);
      const loop = (lockChildren, condition) => {
        const ret = [];
        lockChildren.forEach(child => {
          if (child.children) {
            const res = loop(child.children, condition);
            if (!res.length) {
              ret.push(child);
            }
          } else {
            const order = condition(child);
            if (!order) {
              ret.push(child);
            }
          }
        });
        ret.forEach(res => {
          const index = lockChildren.indexOf(res);
          lockChildren.splice(index, 1);
        });
        return lockChildren;
      };
      loop(lockLeftChildren, child => {
        if (child.lock === true || child.lock === 'left') {
          return 'left';
        }
      });
      loop(lockRightChildren, child => {
        if (child.lock === 'right') {
          return 'right';
        }
      });
      loop(originChildren, child => {
        return (
          child.lock !== true &&
                    child.lock !== 'left' &&
                    child.lock !== 'right'
        );
      });
      return {
        lockLeftChildren,
        lockRightChildren,
        originChildren,
      };
    }

    // 将左侧的锁列树和中间的普通树及右侧的锁列树进行合并
    mergeFromSplitLockChildren(splitChildren) {
      const { lockLeftChildren, lockRightChildren } = splitChildren;
      let { originChildren } = splitChildren;
      Array.prototype.unshift.apply(originChildren, lockLeftChildren);
      originChildren = originChildren.concat(lockRightChildren);
      return originChildren;
    }

    getTableInstance = (type, instance) => {
      type = type ? type.charAt(0).toUpperCase() + type.substr(1) : '';
      this[`table${type}Inc`] = instance;
    };

    getNode = (type, node, lockType) => {
      lockType = lockType
        ? lockType.charAt(0).toUpperCase() + lockType.substr(1)
        : '';
      this[`${type}${lockType}Node`] = node;
      if (type === 'header' && !this.innerHeaderNode && !lockType) {
        this.innerHeaderNode = this.headerNode.querySelector('div');
      }
    };

    onRowMouseEnter = (record, index) => {
      if (this.isLock()) {
        const row = this.getRowNode(index);
        const leftRow = this.getRowNode(index, 'left');
        const rightRow = this.getRowNode(index, 'right');
        [row, leftRow, rightRow].forEach(row => {
          row && dom.addClass(row, 'hovered');
        });
      }
    };

    onRowMouseLeave = (record, index) => {
      if (this.isLock()) {
        const row = this.getRowNode(index);
        const leftRow = this.getRowNode(index, 'left');
        const rightRow = this.getRowNode(index, 'right');
        [row, leftRow, rightRow].forEach(row => {
          row && dom.removeClass(row, 'hovered');
        });
      }
    };

    scroll() {
      const { scrollToCol = 0, scrollToRow = 0 } = this.props;
      if (!scrollToCol && !scrollToRow) {
        return;
      }
      const colCellNode = this.getCellNode(0, scrollToCol);
      const rowCellNode = this.getCellNode(scrollToRow, 0);
      const bodyNodeOffset = this.bodyNode.getBoundingClientRect();
      if (colCellNode) {
        const cellNodeoffset = colCellNode.getBoundingClientRect();
        const scrollLeft = cellNodeoffset.left - bodyNodeOffset.left;
        this.bodyNode.scrollLeft = scrollLeft;
      }
      if (rowCellNode) {
        const cellNodeoffset = rowCellNode.getBoundingClientRect();
        const scrollTop = cellNodeoffset.top - bodyNodeOffset.top;
        this.bodyNode.scrollTop = scrollTop;
      }
    }

    onLockBodyScrollTop = event => {
      // set scroll top for lock columns & main body
      const { target } = event;
      if (event.currentTarget !== target) {
        return;
      }
      const distScrollTop = target.scrollTop;

      if (this.isLock() && distScrollTop !== this.lastScrollTop) {
        const lockRightBody = this.bodyRightNode;
        const lockLeftBody = this.bodyLeftNode;
        const { bodyNode } = this;

        const arr = [lockLeftBody, lockRightBody, bodyNode];

        arr.forEach(node => {
          if (node && node.scrollTop !== distScrollTop) {
            node.scrollTop = distScrollTop;
          }
        });

        this.lastScrollTop = distScrollTop;
      }
    };

    onLockBodyScrollLeft = () => {
      // add shadow class for lock columns
      if (this.isLock()) {
        const { rtl } = this.props;
        const lockRightTable = rtl
          ? this.getWrapperNode('left')
          : this.getWrapperNode('right');
        const lockLeftTable = rtl
          ? this.getWrapperNode('right')
          : this.getWrapperNode('left');
        const shadowClassName = 'shadow';

        const x = this.bodyNode.scrollLeft;

        if (x === 0) {
          lockLeftTable &&
                        dom.removeClass(lockLeftTable, shadowClassName);
          lockRightTable &&
                        dom.addClass(lockRightTable, shadowClassName);
        } else if (
          x ===
                    this.bodyNode.scrollWidth - this.bodyNode.clientWidth
        ) {
          lockLeftTable &&
                        dom.addClass(lockLeftTable, shadowClassName);
          lockRightTable &&
                        dom.removeClass(lockRightTable, shadowClassName);
        } else {
          lockLeftTable &&
                        dom.addClass(lockLeftTable, shadowClassName);
          lockRightTable &&
                        dom.addClass(lockRightTable, shadowClassName);
        }
      }
    };

    onLockBodyScroll = event => {
      this.onLockBodyScrollTop(event);
      this.onLockBodyScrollLeft();
    };

    // Table处理过后真实的lock状态
    isLock() {
      return (
        this.lockLeftChildren.length || this.lockRightChildren.length
      );
    }

    // 用户设置的lock状态
    isOriginLock() {
      return this._isLock;
    }

    adjustSize = () => {
      if (!this.adjustIfTableNotNeedLock()) {
        this.adjustHeaderSize();
        this.adjustBodySize();
        this.adjustRowHeight();
        this.onLockBodyScrollLeft();
      }
    };

    removeLockTable() {
      const lockLeftLen = this.lockLeftChildren.length;
      const lockRightLen = this.lockRightChildren.length;

      if (lockLeftLen) {
        this._notNeedAdjustLockLeft = true;
      }
      if (lockRightLen) {
        this._notNeedAdjustLockRight = true;
      }
      if (lockRightLen || lockLeftLen) {
        this.forceUpdate();
        return true;
      }
    }

    adjustIfTableNotNeedLock() {
      if (this.isOriginLock()) {
        const widthObj = this.tableInc.flatChildren
          .map((item, index) => {
            const cell = this.getCellNode(0, index) || {};
            const headerCell =
                            this.getHeaderCellNode(0, index) || {};

            return {
              cellWidths: cell.clientWidth || 0,
              headerWidths: headerCell.clientWidth || 0,
            };
          })
          .reduce(
            (a, b) => {
              return {
                cellWidths: a.cellWidths + b.cellWidths,
                headerWidths: a.headerWidths + b.headerWidths,
              };
            },
            {
              cellWidths: 0,
              headerWidths: 0,
            },
          );

        let node; let
          width;

        try {
          node = findDOMNode(this);
          width = node.clientWidth;
        } catch (err) {
          node = null;
          width = 0;
        }

        // if the table doesn't exist, there is no need to adjust
        if (width === 0) {
          return true;
        }

        const configWidths =
                    widthObj.cellWidths || widthObj.headerWidths;

        if (configWidths <= width && configWidths > 0) {
          this.removeLockTable();
        } else if (
          this._notNeedAdjustLockLeft ||
                    this._notNeedAdjustLockRight
        ) {
          this._notNeedAdjustLockLeft = this._notNeedAdjustLockRight = false;
          this.forceUpdate();
        } else {
          this._notNeedAdjustLockLeft = this._notNeedAdjustLockRight = false;
          return false;
        }
      }

      return false;
    }

    adjustBodySize() {
      const { rtl, hasHeader } = this.props;
      const header = this.headerNode;
      const paddingName = rtl ? 'paddingLeft' : 'paddingRight';
      const marginName = rtl ? 'marginLeft' : 'marginRight';
      const scrollBarSize = +dom.scrollbar().width || 0;
      const style = {
        [paddingName]: scrollBarSize,
        [marginName]: scrollBarSize,
      };
      const body = this.bodyNode;
      const hasVerScroll = body && body.scrollHeight > body.clientHeight;

      if (this.isLock()) {
        const lockLeftBody = this.bodyLeftNode;
        const lockRightBody = this.bodyRightNode;
        const lockRightBodyWrapper = this.getWrapperNode('right');
        const bodyHeight = body.offsetHeight;
        const width = hasVerScroll ? scrollBarSize : 0;
        const lockBodyHeight = bodyHeight - scrollBarSize;

        if (!hasVerScroll) {
          style[paddingName] = 0;
          style[marginName] = 0;
        }
        if (+scrollBarSize) {
          style.marginBottom = -scrollBarSize;
          style.paddingBottom = scrollBarSize;
        } else {
          style.marginBottom = -20;
          style.paddingBottom = 20;
        }

        const lockStyle = {
          'max-height': lockBodyHeight,
        };
        if (!hasHeader && !+scrollBarSize) {
          lockStyle[marginName] = 0;
        }
        if (+scrollBarSize) {
          lockStyle[marginName] = -scrollBarSize;
        }
        lockLeftBody && dom.setStyle(lockLeftBody, lockStyle);
        lockRightBody && dom.setStyle(lockRightBody, lockStyle);
        lockRightBodyWrapper &&
                    +scrollBarSize &&
                    dom.setStyle(
                      lockRightBodyWrapper,
                      rtl ? 'left' : 'right',
                      `${width}px`,
                    );
      } else {
        style.marginBottom = -scrollBarSize;
        style.paddingBottom = scrollBarSize;
        style[marginName] = 0;
        if (!hasVerScroll) {
          style[paddingName] = 0;
        }
      }

      header && dom.setStyle(header, style);
    }

    adjustHeaderSize() {
      if (this.isLock()) {
        this.tableInc.groupChildren.forEach((child, index) => {
          const lastIndex =
                        this.tableInc.groupChildren[index].length - 1;
          const headerRightRow = this.getHeaderCellNode(
            index,
            lastIndex,
          );
          const headerLeftRow = this.getHeaderCellNode(index, 0);
          const headerRightLockRow = this.getHeaderCellNode(
            index,
            0,
            'right',
          );
          const headerLeftLockRow = this.getHeaderCellNode(
            index,
            0,
            'left',
          );

          if (headerRightRow && headerRightLockRow) {
            const maxRightRowHeight = headerRightRow.offsetHeight;

            dom.setStyle(
              headerRightLockRow,
              'height',
              maxRightRowHeight,
            );

            setTimeout(() => {
              const { affixRef } = this.tableRightInc;
              // if rendered then update postion of affix
              return (
                affixRef &&
                                affixRef.getInstance() &&
                                affixRef.getInstance().updatePosition()
              );
            });
          }

          if (headerLeftRow && headerLeftLockRow) {
            const maxLeftRowHeight = headerLeftRow.offsetHeight;

            dom.setStyle(
              headerLeftLockRow,
              'height',
              maxLeftRowHeight,
            );

            setTimeout(() => {
              const { affixRef } = this.tableLeftInc;
              // if rendered then update postion of affix
              return (
                affixRef &&
                                affixRef.getInstance() &&
                                affixRef.getInstance().updatePosition()
              );
            });
          }
        });
      }
    }

    adjustRowHeight() {
      if (this.isLock()) {
        this.tableInc.props.dataSource.forEach((item, index) => {
          // record may be a string
          const rowIndex =
                        typeof item === 'object' && '__rowIndex' in item
                          ? item.__rowIndex
                          : index;

          // 同步左侧的锁列
          this.setRowHeight(rowIndex, 'left');
          // 同步右侧的锁列
          this.setRowHeight(rowIndex, 'right');
        });
      }
    }

    setRowHeight(rowIndex, dir) {
      const lockRow = this.getRowNode(rowIndex, dir);
      const row = this.getRowNode(rowIndex);
      const rowHeight =
                    (ieVersion
                      ? row && row.offsetHeight
                      : row && parseFloat(getComputedStyle(row).height)) ||
                    'auto';
      const lockHeight =
                    (ieVersion
                      ? lockRow && lockRow.offsetHeight
                      : lockRow &&
                          parseFloat(getComputedStyle(lockRow).height)) ||
                    'auto';

      if (lockRow && rowHeight !== lockHeight) {
        dom.setStyle(lockRow, 'height', rowHeight);
      }
    }

    getWrapperNode(type) {
      type = type ? type.charAt(0).toUpperCase() + type.substr(1) : '';
      try {
        // in case of finding an unmounted component due to cached data
        // need to clear refs of table when dataSource Changed
        // use try catch for temporary
        return findDOMNode(this.refs[`lock${type}`]);
      } catch (error) {
        return null;
      }
    }

    // remove this in next major version, keep this for temperary incase of using it
    // getFirstNormalCellNode(index) {
    //     let i = 0;
    //     let row;
    //     do {
    //         row = this.getCellNode(index, i);
    //         i++;
    //     } while (
    //         (!row || (row && row.rowSpan && row.rowSpan > 1)) &&
    //         this.tableInc.flatChildren.length > i
    //     );

    //     return row;
    // }

    getRowNode(index, type) {
      type = type ? type.charAt(0).toUpperCase() + type.substr(1) : '';
      const table = this[`table${type}Inc`];

      try {
        // in case of finding an unmounted component due to cached data
        // need to clear refs of table when dataSource Changed
        // use try catch for temporary
        return findDOMNode(table.getRowRef(index));
      } catch (error) {
        return null;
      }
    }

    getHeaderCellNode(index, i, type) {
      type = type ? type.charAt(0).toUpperCase() + type.substr(1) : '';
      const table = this[`table${type}Inc`];

      try {
        // in case of finding an unmounted component due to cached data
        // need to clear refs of table when dataSource Changed
        // use try catch for temporary
        return findDOMNode(table.getHeaderCellRef(index, i));
      } catch (error) {
        return null;
      }
    }

    getCellNode(index, i, type) {
      type = type ? type.charAt(0).toUpperCase() + type.substr(1) : '';
      const table = this[`table${type}Inc`];

      try {
        // in case of finding an unmounted component due to cached data
        // need to clear refs of table when dataSource Changed
        // use try catch for temporary
        return findDOMNode(table.getCellRef(index, i));
      } catch (error) {
        return null;
      }
    }

    getFlatenChildrenLength = (children = []) => {
      const loop = arr => {
        const newArray = [];
        arr.forEach(child => {
          if (child.children) {
            newArray.push(...loop(child.children));
          } else {
            newArray.push(child);
          }
        });
        return newArray;
      };

      return loop(children).length;
    };

    render() {
      /* eslint-disable no-unused-vars, prefer-const */
      let {
        children,
        prefix,
        components,
        className,
        dataSource,
        ...others
      } = this.props;
      let {
        lockLeftChildren,
        lockRightChildren,
        children: normalizedChildren,
      } = this.normalizeChildrenState(this.props);

      const leftLen = this.getFlatenChildrenLength(lockLeftChildren);
      const rightLen = this.getFlatenChildrenLength(lockRightChildren);
      const originLen = this.getFlatenChildrenLength(normalizedChildren);

      const lengths = {
        left: leftLen,
        right: rightLen,
        origin: originLen,
      };
      if (this._notNeedAdjustLockLeft) {
        lockLeftChildren = [];
      }
      if (this._notNeedAdjustLockRight) {
        lockRightChildren = [];
      }
      this.lockLeftChildren = lockLeftChildren;
      this.lockRightChildren = lockRightChildren;

      if (this.isOriginLock()) {
        components = { ...components };
        components.Body = components.Body || LockBody;
        components.Header = components.Header || LockHeader;
        components.Wrapper = components.Wrapper || LockWrapper;
        components.Row = components.Row || LockRow;
        className = classnames({
          [`${prefix}table-lock`]: true,
          [`${prefix}table-wrap-empty`]: !dataSource.length,
          [className]: className,
        });
        const content = [
          <BaseComponent
            {...others}
            dataSource={dataSource}
            key="lock-left"
            columns={lockLeftChildren}
            className={`${prefix}table-lock-left`}
            lengths={lengths}
            prefix={prefix}
            lockType="left"
            components={components}
            ref="lockLeft"
            loading={false}
            aria-hidden
          />,
          <BaseComponent
            {...others}
            dataSource={dataSource}
            key="lock-right"
            columns={lockRightChildren}
            className={`${prefix}table-lock-right`}
            lengths={lengths}
            prefix={prefix}
            lockType="right"
            components={components}
            ref="lockRight"
            loading={false}
            aria-hidden
          />,
        ];
        return (
          <BaseComponent
            {...others}
            dataSource={dataSource}
            columns={normalizedChildren}
            prefix={prefix}
            lengths={lengths}
            wrapperContent={content}
            components={components}
            className={className}
          />
        );
      }
      return <BaseComponent {...this.props} />;
    }
  }
  statics(LockTable, BaseComponent);
  return LockTable;
}

function deepCopy(arr) {
  let copy = arr => {
    return arr.map(item => {
      const newItem = { ...item };
      if (item.children) {
        item.children = copy(item.children);
      }
      return newItem;
    });
  };
  return copy(arr);
}
