import * as React from "react";
// import PropTypes from "prop-types";
import classNames from "classnames";

import { TreeContext } from "./context-types";
import {
  getDataAndAria,
  getDragNodesKeys,
  conductExpandParent,
  calcSelectedKeys,
  calcDropPosition,
  arrAdd,
  arrDel,
  posToArr
} from "./util";
import {
  DataNode,
  IconType,
  Key,
  FlattenNode,
  DataEntity,
  EventDataNode,
  NodeInstance,
  ScrollTo
} from "./interface";
import {
  flattenTreeData,
  convertTreeToData,
  convertDataToEntities,
  convertNodePropsToEventData
} from "./util/tree-util";
import { NodeList, MOTION_KEY, MotionEntity, NodeListRef } from "./node-list";
import TreeNode from "./tree-node";

interface TreeProps {
  prefixCls: string;
  className?: string;
  style?: React.CSSProperties;
  focusable?: boolean;
  tabIndex?: number;
  children?: React.ReactNode;
  treeData?: DataNode[]; // Generate treeNode by children
  showLine?: boolean;
  showIcon?: boolean;
  icon?: IconType;
  selectable?: boolean;
  disabled?: boolean;
  multiple?: boolean;
  draggable?: boolean;
  defaultExpandParent?: boolean;
  autoExpandParent?: boolean;
  defaultExpandAll?: boolean;
  defaultExpandedKeys?: Key[];
  expandedKeys?: Key[];
  defaultCheckedKeys?: Key[];
  defaultSelectedKeys?: Key[];
  selectedKeys?: Key[];
  onFocus?: React.FocusEventHandler<HTMLDivElement>;
  onBlur?: React.FocusEventHandler<HTMLDivElement>;
  // onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
  onClick?: (e: React.MouseEvent, treeNode: EventDataNode) => void;
  onDoubleClick?: (e: React.MouseEvent, treeNode: EventDataNode) => void;
  onExpand?: (
    expandedKeys: Key[],
    info: {
      node: EventDataNode;
      expanded: boolean;
      nativeEvent: MouseEvent;
    }
  ) => void;
  onSelect?: (
    selectedKeys: Key[],
    info: {
      event: "select";
      selected: boolean;
      node: EventDataNode;
      selectedNodes: DataNode[];
      nativeEvent: MouseEvent;
    }
  ) => void;
  onLoad?: (
    loadedKeys: Key[],
    info: {
      event: "load";
      node: EventDataNode;
    }
  ) => void;
  loadData?: (treeNode: EventDataNode) => Promise<void>;
  loadedKeys?: Key[];
  onMouseEnter?: (info: {
    event: React.MouseEvent;
    node: EventDataNode;
  }) => void;
  onMouseLeave?: (info: {
    event: React.MouseEvent;
    node: EventDataNode;
  }) => void;
  onRightClick?: (info: {
    event: React.MouseEvent;
    node: EventDataNode;
  }) => void;
  onDragStart?: (info: {
    event: React.MouseEvent;
    node: EventDataNode;
  }) => void;
  onDragEnter?: (info: {
    event: React.MouseEvent;
    node: EventDataNode;
    expandedKeys: Key[];
  }) => void;
  onDragOver?: (info: { event: React.MouseEvent; node: EventDataNode }) => void;
  onDragLeave?: (info: {
    event: React.MouseEvent;
    node: EventDataNode;
  }) => void;
  onDragEnd?: (info: { event: React.MouseEvent; node: EventDataNode }) => void;
  onDrop?: (info: {
    event: React.MouseEvent;
    node: EventDataNode;
    dragNode: EventDataNode;
    dragNodesKeys: Key[];
    dropPosition: number;
    dropToGap: boolean;
  }) => void;
  /**
   * Do not use in your production code directly since this will be refactor.
   */
  onActiveChange?: (key: Key) => void;
  filterTreeNode?: (treeNode: EventDataNode) => boolean;
  motion?: any;
  switcherIcon?: IconType;

  // Virtual List
  height?: number;
  itemHeight?: number;
}

interface TreeState {
  keyEntities: Record<Key, DataEntity>;

  selectedKeys: Key[];
  loadedKeys: Key[];
  loadingKeys: Key[];
  expandedKeys: Key[];

  dragging: boolean;
  dragNodesKeys: Key[];
  dragOverNodeKey: Key;
  dropPosition: number;

  treeData: DataNode[];
  flattenNodes: FlattenNode[];

  focused: boolean;
  activeKey: Key;

  prevProps: TreeProps;
}

// const keyPropType = PropTypes.oneOfType([PropTypes.string, PropTypes.number]);

class Tree extends React.Component<TreeProps, TreeState> {
  // static propTypes = {
  //   prefixCls: PropTypes.string,
  //   className: PropTypes.string,
  //   style: PropTypes.object,
  //   tabIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  //   children: PropTypes.any,
  //   treeData: PropTypes.array, // Generate treeNode by children
  //   showLine: PropTypes.bool,
  //   showIcon: PropTypes.bool,
  //   icon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
  //   selectable: PropTypes.bool,
  //   disabled: PropTypes.bool,
  //   multiple: PropTypes.bool,
  //   draggable: PropTypes.bool,
  //   defaultExpandParent: PropTypes.bool,
  //   autoExpandParent: PropTypes.bool,
  //   defaultExpandAll: PropTypes.bool,
  //   defaultExpandedKeys: PropTypes.arrayOf(keyPropType),
  //   expandedKeys: PropTypes.arrayOf(keyPropType),
  //   defaultSelectedKeys: PropTypes.arrayOf(keyPropType),
  //   selectedKeys: PropTypes.arrayOf(keyPropType),
  //   onClick: PropTypes.func,
  //   onDoubleClick: PropTypes.func,
  //   onExpand: PropTypes.func,
  //   onSelect: PropTypes.func,
  //   onLoad: PropTypes.func,
  //   loadData: PropTypes.func,
  //   loadedKeys: PropTypes.arrayOf(keyPropType),
  //   onMouseEnter: PropTypes.func,
  //   onMouseLeave: PropTypes.func,
  //   onRightClick: PropTypes.func,
  //   onDragStart: PropTypes.func,
  //   onDragEnter: PropTypes.func,
  //   onDragOver: PropTypes.func,
  //   onDragLeave: PropTypes.func,
  //   onDragEnd: PropTypes.func,
  //   onDrop: PropTypes.func,
  //   filterTreeNode: PropTypes.func,
  //   motion: PropTypes.object,
  //   switcherIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.func])
  // };

  static defaultProps = {
    prefixCls: "co-tree",
    showLine: false,
    showIcon: true,
    selectable: true,
    multiple: false,
    disabled: false,
    draggable: false,
    defaultExpandParent: true,
    autoExpandParent: false,
    defaultExpandAll: false,
    defaultExpandedKeys: [],
    defaultSelectedKeys: []
  };

  static TreeNode = TreeNode;

  delayedDragEnterLogic: Record<Key, number>;

  state: TreeState = {
    keyEntities: {},

    selectedKeys: [],
    loadedKeys: [],
    loadingKeys: [],
    expandedKeys: [],

    dragging: false,
    dragNodesKeys: [],
    dragOverNodeKey: null,
    dropPosition: null,

    treeData: [],
    flattenNodes: [],

    focused: false,
    activeKey: null,

    prevProps: null
  };

  dragNode: NodeInstance;

  listRef = React.createRef<NodeListRef>();

  static getDerivedStateFromProps(props: TreeProps, prevState: TreeState) {
    const { prevProps } = prevState; //用之前的静态state表示旧props
    const newState: Partial<TreeState> = {
      prevProps: props
    }; //定义一个新的state（取自旧state），再将其指定给props（函数原始继承的props）

    const needSync = (name: string) => {
      return (
        (!prevProps && name in props) ||
        (prevProps && prevProps[name] !== props[name])
      );
    };

    // ================== Tree Node ==================
    let treeData: DataNode[];

    // Check if `treeData` or `children` changed and save into the state.
    if (needSync("treeData")) {
      ({ treeData } = props);
    } else if (needSync("children")) {
      treeData = convertTreeToData(props.children);
    }

    // Save flatten nodes info and convert `treeData` into keyEntities
    if (treeData) {
      newState.treeData = treeData;
      const entitiesMap = convertDataToEntities(treeData);
      newState.keyEntities = {
        [MOTION_KEY]: MotionEntity,
        ...entitiesMap.keyEntities
      };
    }

    const keyEntities = newState.keyEntities || prevState.keyEntities;

    // ================ expandedKeys =================
    if (
      needSync("expandedKeys") ||
      (prevProps && needSync("autoExpandParent"))
    ) {
      newState.expandedKeys =
        props.autoExpandParent || (!prevProps && props.defaultExpandParent)
          ? conductExpandParent(props.expandedKeys, keyEntities)
          : props.expandedKeys;
    } else if (!prevProps && props.defaultExpandAll) {
      const cloneKeyEntities = { ...keyEntities };
      delete cloneKeyEntities[MOTION_KEY];
      newState.expandedKeys = Object.keys(cloneKeyEntities).map(
        key => cloneKeyEntities[key].key
      );
    } else if (!prevProps && props.defaultExpandedKeys) {
      newState.expandedKeys =
        props.autoExpandParent || props.defaultExpandParent
          ? conductExpandParent(props.defaultExpandedKeys, keyEntities)
          : props.defaultExpandedKeys;
    }

    if (!newState.expandedKeys) {
      delete newState.expandedKeys;
    }

    // ================ flattenNodes =================
    if (treeData || newState.expandedKeys) {
      const flattenNodes: FlattenNode[] = flattenTreeData(
        treeData || prevState.treeData,
        newState.expandedKeys || prevState.expandedKeys
      );
      newState.flattenNodes = flattenNodes;
    }

    // ================ selectedKeys =================
    if (props.selectable) {
      if (needSync("selectedKeys")) {
        newState.selectedKeys = calcSelectedKeys(props.selectedKeys, props);
      } else if (!prevProps && props.defaultSelectedKeys) {
        newState.selectedKeys = calcSelectedKeys(
          props.defaultSelectedKeys,
          props
        );
      }
    }

    // ================= loadedKeys ==================
    if (needSync("loadedKeys")) {
      newState.loadedKeys = props.loadedKeys;
    }

    return newState;
  }

  onNodeDragStart = (
    event: React.MouseEvent<HTMLDivElement>,
    node: NodeInstance
  ) => {
    const { expandedKeys, keyEntities } = this.state;
    const { onDragStart } = this.props;
    const { eventKey } = node.props;

    this.dragNode = node;

    this.setState({
      dragging: true,
      dragNodesKeys: getDragNodesKeys(eventKey, keyEntities),
      expandedKeys: arrDel(expandedKeys, eventKey)
    });

    if (onDragStart) {
      onDragStart({ event, node: convertNodePropsToEventData(node.props) });
    }
  };

  /**
   * Select handler is less small than node,
   * so that this will trigger when drag enter node or select handler.
   * This is a little tricky if customize css without padding.
   * Better for use mouse move event to refresh drag state.
   * But let's just keep it to avoid event trigger logic change.
   */
  onNodeDragEnter = (
    event: React.MouseEvent<HTMLDivElement>,
    node: NodeInstance
  ) => {
    const { expandedKeys, keyEntities } = this.state;
    const { onDragEnter } = this.props;
    const { pos, eventKey } = node.props;

    if (!this.dragNode) return;

    const dropPosition = calcDropPosition(event, node);

    // Skip if drag node is self
    if (this.dragNode.props.eventKey === eventKey && dropPosition === 0) {
      this.setState({
        dragOverNodeKey: "",
        dropPosition: null
      });
      return;
    }

    // Add timeout to let onDragLevel fire before onDragEnter,
    // so that we can clean drag props for onDragLeave node.
    // Macro task for this:

    setTimeout(() => {
      // Update drag over node
      this.setState({
        dragOverNodeKey: eventKey,
        dropPosition
      });

      // Side effect for delay drag
      if (!this.delayedDragEnterLogic) {
        this.delayedDragEnterLogic = {};
      }
      Object.keys(this.delayedDragEnterLogic).forEach(key => {
        clearTimeout(this.delayedDragEnterLogic[key]);
      });
      this.delayedDragEnterLogic[pos] = window.setTimeout(() => {
        if (!this.state.dragging) return;

        let newExpandedKeys = [...expandedKeys];
        const entity = keyEntities[eventKey];

        if (entity && (entity.children || []).length) {
          newExpandedKeys = arrAdd(expandedKeys, eventKey);
        }

        if (!("expandedKeys" in this.props)) {
          this.setState({
            expandedKeys: newExpandedKeys
          });
        }

        if (onDragEnter) {
          onDragEnter({
            event,
            node: convertNodePropsToEventData(node.props),
            expandedKeys: newExpandedKeys
          });
        }
      }, 400);
    }, 0);
  };

  onNodeDragOver = (
    event: React.MouseEvent<HTMLDivElement>,
    node: NodeInstance
  ) => {
    const { onDragOver } = this.props;
    const { eventKey } = node.props;

    // Update drag position
    if (this.dragNode && eventKey === this.state.dragOverNodeKey) {
      const dropPosition = calcDropPosition(event, node);

      if (dropPosition === this.state.dropPosition) return;

      this.setState({
        dropPosition
      });
    }

    if (onDragOver) {
      onDragOver({ event, node: convertNodePropsToEventData(node.props) });
    }
  };

  onNodeDragLeave = (
    event: React.MouseEvent<HTMLDivElement>,
    node: NodeInstance
  ) => {
    const { onDragLeave } = this.props;

    this.setState({
      dragOverNodeKey: ""
    });

    if (onDragLeave) {
      onDragLeave({ event, node: convertNodePropsToEventData(node.props) });
    }
  };

  onNodeDragEnd = (
    event: React.MouseEvent<HTMLDivElement>,
    node: NodeInstance
  ) => {
    const { onDragEnd } = this.props;
    this.setState({
      dragOverNodeKey: ""
    });
    this.cleanDragState();

    if (onDragEnd) {
      onDragEnd({ event, node: convertNodePropsToEventData(node.props) });
    }

    this.dragNode = null;
  };

  onNodeDrop = (
    event: React.MouseEvent<HTMLDivElement>,
    node: NodeInstance
  ) => {
    const { dragNodesKeys = [], dropPosition } = this.state;
    const { onDrop } = this.props;
    const { eventKey, pos } = node.props;

    this.setState({
      dragOverNodeKey: ""
    });
    this.cleanDragState();

    if (dragNodesKeys.indexOf(eventKey) !== -1) {
      return;
    }

    const posArr = posToArr(pos);

    const dropResult = {
      event,
      node: convertNodePropsToEventData(node.props),
      dragNode: convertNodePropsToEventData(this.dragNode.props),
      dragNodesKeys: dragNodesKeys.slice(),
      dropPosition: dropPosition + Number(posArr[posArr.length - 1]),
      dropToGap: false
    };

    if (dropPosition !== 0) {
      dropResult.dropToGap = true;
    }

    if (onDrop) {
      onDrop(dropResult);
    }

    this.dragNode = null;
  };

  cleanDragState = () => {
    const { dragging } = this.state;
    if (dragging) {
      this.setState({
        dragging: false
      });
    }
  };

  onNodeClick = (
    e: React.MouseEvent<HTMLDivElement>,
    treeNode: EventDataNode
  ) => {
    const { onClick } = this.props;
    if (onClick) {
      onClick(e, treeNode);
    }
  };

  onNodeDoubleClick = (
    e: React.MouseEvent<HTMLDivElement>,
    treeNode: EventDataNode
  ) => {
    const { onDoubleClick } = this.props;
    if (onDoubleClick) {
      onDoubleClick(e, treeNode);
    }
  };

  onNodeSelect = (
    e: React.MouseEvent<HTMLDivElement>,
    treeNode: EventDataNode
  ) => {
    let { selectedKeys } = this.state;
    const { keyEntities } = this.state;
    const { onSelect, multiple } = this.props;
    const { selected, key } = treeNode;
    const targetSelected = !selected;

    // Update selected keys
    if (!targetSelected) {
      selectedKeys = arrDel(selectedKeys, key);
    } else if (!multiple) {
      selectedKeys = [key];
    } else {
      selectedKeys = arrAdd(selectedKeys, key);
    }

    //Not found related usage in doc or upper libs
    const selectedNodes = selectedKeys
      .map(selectedKey => {
        const entity = keyEntities[selectedKey];
        if (!entity) return null;

        return entity.node;
      })
      .filter(node => node);

    this.setUncontrolledState({ selectedKeys });

    if (onSelect) {
      onSelect(selectedKeys, {
        event: "select",
        selected: targetSelected,
        node: treeNode,
        selectedNodes,
        nativeEvent: e.nativeEvent
      });
    }
  };

  onNodeLoad = (treeNode: EventDataNode) =>
    new Promise(resolve => {
      // We need to get the latest state of loading/loaded keys
      this.setState(({ loadedKeys = [], loadingKeys = [] }): any => {
        const { loadData, onLoad } = this.props;
        const { key } = treeNode;

        if (
          !loadData ||
          loadedKeys.indexOf(key) !== -1 ||
          loadingKeys.indexOf(key) !== -1
        ) {
          // react 15 will warn if return null
          return {};
        }

        // Process load data
        const promise = loadData(treeNode);
        promise.then(() => {
          const {
            loadedKeys: currentLoadedKeys,
            loadingKeys: currentLoadingKeys
          } = this.state;
          const newLoadedKeys = arrAdd(currentLoadedKeys, key);
          const newLoadingKeys = arrDel(currentLoadingKeys, key);

          // onLoad should trigger before internal setState to avoid `loadData` trigger twice.
          if (onLoad) {
            onLoad(newLoadedKeys, {
              event: "load",
              node: treeNode
            });
          }

          this.setUncontrolledState({
            loadedKeys: newLoadedKeys
          });
          this.setState({
            loadingKeys: newLoadingKeys
          });

          resolve();
        });

        return {
          loadingKeys: arrAdd(loadingKeys, key)
        };
      });
    });

  onNodeExpand = (
    e: React.MouseEvent<HTMLDivElement>,
    treeNode: EventDataNode
  ) => {
    let { expandedKeys } = this.state;
    const { treeData } = this.state;
    const { onExpand, loadData } = this.props;
    const { key, expanded } = treeNode;

    // Update selected keys
    const targetExpanded = !expanded;

    if (targetExpanded) {
      expandedKeys = arrAdd(expandedKeys, key);
    } else {
      expandedKeys = arrDel(expandedKeys, key);
    }

    const flattenNodes: FlattenNode[] = flattenTreeData(treeData, expandedKeys);
    this.setUncontrolledState({ expandedKeys, flattenNodes }, true);

    if (onExpand) {
      onExpand(expandedKeys, {
        node: treeNode,
        expanded: targetExpanded,
        nativeEvent: e.nativeEvent
      });
    }

    // Async Load data
    if (targetExpanded && loadData) {
      const loadPromise = this.onNodeLoad(treeNode);
      return loadPromise
        ? loadPromise.then(() => {
            // [Legacy] Refresh logic
            const newFlattenTreeData = flattenTreeData(
              this.state.treeData,
              expandedKeys
            );
            this.setUncontrolledState({ flattenNodes: newFlattenTreeData });
          })
        : null;
    }

    return null;
  };

  onNodeMouseEnter = (
    event: React.MouseEvent<HTMLDivElement>,
    node: EventDataNode
  ) => {
    const { onMouseEnter } = this.props;
    if (onMouseEnter) {
      onMouseEnter({ event, node });
    }
  };

  onNodeMouseLeave = (
    event: React.MouseEvent<HTMLDivElement>,
    node: EventDataNode
  ) => {
    const { onMouseLeave } = this.props;
    if (onMouseLeave) {
      onMouseLeave({ event, node });
    }
  };

  onNodeContextMenu = (
    event: React.MouseEvent<HTMLDivElement>,
    node: EventDataNode
  ) => {
    const { onRightClick } = this.props;
    if (onRightClick) {
      event.preventDefault();
      onRightClick({ event, node });
    }
  };

  onFocus: React.FocusEventHandler<HTMLDivElement> = (...args) => {
    const { onFocus } = this.props;
    this.setState({ focused: true });

    if (onFocus) {
      onFocus(...args);
    }
  };

  onBlur: React.FocusEventHandler<HTMLDivElement> = (...args) => {
    const { onBlur } = this.props;
    this.setState({ focused: false });
    this.onActiveChange(null);

    if (onBlur) {
      onBlur(...args);
    }
  };

  getTreeNodeRequiredProps = () => {
    const {
      expandedKeys,
      selectedKeys,
      loadedKeys,
      loadingKeys,
      dragOverNodeKey,
      dropPosition,
      keyEntities
    } = this.state;
    return {
      expandedKeys: expandedKeys || [],
      selectedKeys: selectedKeys || [],
      loadedKeys: loadedKeys || [],
      loadingKeys: loadingKeys || [],
      dragOverNodeKey,
      dropPosition,
      keyEntities
    };
  };

  // spliting line

  onActiveChange = (activeKey: Key) => {
    const { onActiveChange } = this.props;

    this.setState({ activeKey });
    if (activeKey !== null) {
      this.scrollTo({ key: activeKey });
    }

    if (onActiveChange) {
      onActiveChange(activeKey);
    }
  };

  getActiveItem = () => {
    const { activeKey, flattenNodes } = this.state;
    if (activeKey === null) {
      return null;
    }

    return flattenNodes.find(({ data: { key } }) => key === activeKey) || null;
  };

  /**
   * Only update the value which is not in props
   */
  setUncontrolledState = (
    state: Partial<TreeState>,
    atomic: boolean = false
  ) => {
    let needSync = false;
    let allPassed = true;
    const newState = {};

    Object.keys(state).forEach(name => {
      if (name in this.props) {
        allPassed = false;
        return;
      }

      needSync = true;
      newState[name] = state[name];
    });

    if (needSync && (!atomic || allPassed)) {
      this.setState(newState);
    }
  };

  scrollTo: ScrollTo = scroll => {
    this.listRef.current.scrollTo(scroll);
  };

  render() {
    const {
      focused,
      flattenNodes,
      keyEntities,
      dragging,
      activeKey
    } = this.state;
    const {
      prefixCls,
      className,
      style,
      showLine,
      focusable,
      tabIndex = 0,
      selectable,
      showIcon,
      icon,
      switcherIcon,
      draggable,
      disabled,
      motion,
      loadData,
      filterTreeNode,
      height,
      itemHeight
    } = this.props;
    const domProps: React.HTMLAttributes<HTMLDivElement> = getDataAndAria(
      this.props
    );

    return (
      <TreeContext.Provider
        value={{
          prefixCls,
          selectable,
          showIcon,
          icon,
          switcherIcon,
          draggable,
          disabled,
          keyEntities,

          loadData,
          filterTreeNode,

          onNodeClick: this.onNodeClick,
          onNodeDoubleClick: this.onNodeDoubleClick,
          onNodeExpand: this.onNodeExpand,
          onNodeSelect: this.onNodeSelect,
          onNodeLoad: this.onNodeLoad,
          onNodeMouseEnter: this.onNodeMouseEnter,
          onNodeMouseLeave: this.onNodeMouseLeave,
          onNodeContextMenu: this.onNodeContextMenu,
          onNodeDragStart: this.onNodeDragStart,
          onNodeDragEnter: this.onNodeDragEnter,
          onNodeDragOver: this.onNodeDragOver,
          onNodeDragLeave: this.onNodeDragLeave,
          onNodeDragEnd: this.onNodeDragEnd,
          onNodeDrop: this.onNodeDrop
        }}
      >
        <div
          className={classNames(prefixCls, className, {
            [`${prefixCls}-show-line`]: showLine,
            [`${prefixCls}-focused`]: focused,
            [`${prefixCls}-active-focused`]: activeKey !== null
          })}
        >
          <NodeList
            ref={this.listRef}
            prefixCls={prefixCls}
            style={style}
            data={flattenNodes}
            disabled={disabled}
            selectable={selectable}
            motion={motion}
            dragging={dragging}
            height={height}
            itemHeight={itemHeight}
            focusable={focusable}
            focused={focused}
            tabIndex={tabIndex}
            activeItem={this.getActiveItem()}
            onFocus={this.onFocus}
            onBlur={this.onBlur}
            onActiveChange={this.onActiveChange}
            {...this.getTreeNodeRequiredProps()}
            {...domProps}
          />
        </div>
      </TreeContext.Provider>
    );
  }
}
export { TreeProps };
export default Tree;
