import * as React from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
//@ts-ignore
import {
  DataNode,
  IconType,
  Key,
  EventDataNode,
  DataEntity,
  FlattenNode,
  NodeInstance,
  ScrollTo
} from "./interface";
import {
  getDragNodesKeys,
  arrDel,
  calcDropPosition,
  arrAdd,
  posToArr,
  getDataAndAria,
  conductExpandParent,
  calcSelectedKeys
} from "./util";
import {
  convertNodePropsToEventData,
  flattenTreeData,
  convertTreeToData,
  convertDataToEntities
} from "./util/tree-util";
import { NodeList, MOTION_KEY, MotionEntity, NodeListRef } from "./node-list";
import { TreeContext } from "./context-types";

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;

  //extra state-props
  keyEntities: Record<Key, DataEntity>;
}

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]);

const Tree: React.SFC<TreeProps> = props => {
  Tree.propTypes = {
    prefixCls: PropTypes.string,
    className: PropTypes.string,
    style: PropTypes.object,
    tabIndex: PropTypes.number,
    children: PropTypes.any,
    treeData: PropTypes.array,
    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])
  };

  Tree.defaultProps = {
    prefixCls: "co-tree",
    showLine: false,
    showIcon: true,
    selectable: true,
    multiple: false,
    disabled: false,
    draggable: false,
    defaultExpandParent: true,
    autoExpandParent: false,
    defaultExpandAll: false,
    defaultExpandedKeys: [],
    defaultSelectedKeys: []
  };
  //   const [keyEntities, setKeyEntities] = React.useState<TreeProps>();
  const [selectedKeys, setSelectedKeys] = React.useState<Key[]>([]);
  const [loadedKeys, setLoadedKeys] = React.useState<Key[]>([]);
  const [loadingKeys, setLoadingKeys] = React.useState<Key[]>([]);
  const [expandedKeys, setExpandedKeys] = React.useState<Key[]>([]);
  const [dragging, setDragging] = React.useState<boolean>(false);
  const [dragNodesKeys, setDragNodesKeys] = React.useState<Key[]>([]);
  const [dragOverNodeKey, setDragOverNodeKey] = React.useState<Key>();
  const [dropPosition, setDropPosition] = React.useState<number>(null);
  const [treeData, setTreeData] = React.useState<DataNode[]>([]);
  const [flattenNodes, setFlattenNode] = React.useState<FlattenNode[]>([]);
  const [focused, setFocused] = React.useState<boolean>(false);
  const [activeKey, setActiveKey] = React.useState<Key>(null);
  //   const [prevProps, setPrevProps] = React.useState<TreeProps>(null);

  let dragNode: NodeInstance;
  let delayedDragEnterLogic: Record<Key, number>;
  let listRef = React.createRef<NodeListRef>();

  React.useCallback(() => {
    let prevProps: TreeState;

    const newState: Partial<TreeState> = {
      prevProps: props
    };

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

    if (needSync("treeData")) {
      setTreeData([]);
    } else if (needSync("children")) {
      setTreeData(convertTreeToData(props.children));
    }

    if (treeData) {
      newState.treeData = treeData;
      const entitiesMap = convertDataToEntities(treeData);
      newState.keyEntities = {
        [MOTION_KEY]: MotionEntity,
        ...entitiesMap.keyEntities
      };
    }

    const keyEntity = newState.keyEntities || props.keyEntities;

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

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

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

    if (props.selectable) {
      if (needSync("selectedKeys")) {
        setSelectedKeys(calcSelectedKeys(props.selectedKeys, props));
      } else if (!prevProps && props.defaultSelectedKeys) {
        setSelectedKeys(calcSelectedKeys(props.defaultExpandedKeys, props));
      }
    }

    if (needSync("loadedKeys")) {
      setLoadedKeys(props.loadedKeys);
    }
  }, null);

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

    setDragging(true);
    setDragNodesKeys(() => getDragNodesKeys(eventKey, keyEntities)),
      setExpandedKeys(() => arrDel(expandedKeys, eventKey));

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

  const onNodeDragEnter = (
    event: React.MouseEvent<HTMLDivElement>,
    node: NodeInstance
  ) => {
    const keyEntities = props.keyEntities;
    const { onDragEnter } = props;
    const { pos, eventKey } = node.props;

    if (!dragNode) return;
    const dropPosition = calcDropPosition(event, node);

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

    setTimeout(() => {
      setDragOverNodeKey(eventKey), setDropPosition(null);

      if (!delayedDragEnterLogic) {
        delayedDragEnterLogic = {};
      }
      Object.keys(delayedDragEnterLogic).forEach(key => {
        clearTimeout(delayedDragEnterLogic[key]);
      });

      delayedDragEnterLogic[pos] = window.setTimeout(() => {
        if (dragging) return;

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

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

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

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

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

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

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

    setDragOverNodeKey("");

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

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

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

    dragNode = null;
  };

  const onNodeDrop = (
    event: React.MouseEvent<HTMLDivElement>,
    node: NodeInstance
  ) => {
    const { onDrop } = props;
    const { eventKey, pos } = node.props;

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

    const posArr = posToArr(pos);

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

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

    if (onDrop) {
      onDrop(dropResult);
    }

    dragNode = null;
  };

  const cleanDragState = () => {
    if (dragging) {
      setDragging(false);
    }
  };

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

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

  const onNodeSelect = (
    e: React.MouseEvent<HTMLDivElement>,
    treeNode: EventDataNode
  ) => {
    let $selectedKeys = selectedKeys;
    const keyEntities = props.keyEntities;
    const { onSelect, multiple } = 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);

    setUncontrolledState({ selectedKeys });

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

  const onNodeLoad = (treeNode: EventDataNode) =>
    new Promise(resolve => {
      ({ loadedKeys = [], loadingKeys = [] }): any => {
        // const [loadData, onLoad] = props;
        const loadData = props.loadData;
        const onLoad = props.onLoad;

        const { key } = treeNode;
        if (
          !loadData ||
          loadedKeys.indexOf(key) !== -1 ||
          loadingKeys.indexOf(key) !== -1
        ) {
          return {};
        }

        const promise = loadData(treeNode);
        promise.then(() => {
          const newLoadedKeys = arrAdd(loadedKeys, key);
          const newLoadingKeys = arrDel(loadingKeys, key);

          if (onLoad) {
            onLoad(newLoadedKeys, {
              event: "load",
              node: treeNode
            });
          }

          setUncontrolledState({
            loadedKeys: newLoadedKeys
          });
          setLoadingKeys(newLoadingKeys);
          resolve();
        });

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

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

    const targetExpanded = !expanded;

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

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

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

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

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

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

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

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

    if (onFocus) {
      onFocus(...args);
    }
  };
  const onBlur: React.FocusEventHandler<HTMLDivElement> = (...args) => {
    const { onBlur } = props;
    setFocused(false);
    onActiveChange(null);

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

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

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

    setActiveKey(activeKey);
    if (activeKey !== null) {
      scrollTo({ key: activeKey });
    }

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

  const getActiveItem = () => {
    if (activeKey === null) {
      return null;
    }

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

  //   const offsetActiveKey = (offset: number) => {
  //     let index = flattenNodes.findIndex(
  //       ({ data: { key } }) => key === activeKey
  //     );

  //     //Align with index
  //     if (index === -1 && offset < 0) {
  //       index = flattenNodes.length;
  //     }

  //     index = (index + offset + flattenNodes.length) % flattenNodes.length;

  //     const item = flattenNodes[index];
  //     if (item) {
  //       const { key } = item.data;

  //       onActiveChange(key);
  //     } else {
  //       onActiveChange(null);
  //     }
  //   };

  //Only update the value which is not in props

  const setUncontrolledState = (
    state: Partial<TreeState>,
    atomic: boolean = false
  ) => {
    let needSync = false;
    let allPassed = true;
    const [newState, setNewState] = React.useState<{}>({});

    Object.keys(state).forEach(name => {
      if (name in props) {
        allPassed = false;
        return;
      }
      needSync = true;
      newState[name] = state[name];
    });

    if (needSync && (!atomic || allPassed)) {
      setNewState(newState);
    }
  };

  const scrollTo: ScrollTo = scroll => {
    listRef.current.scrollTo(scroll);
  };
  const {
    prefixCls,
    className,
    style,
    showLine,
    focusable,
    tabIndex = 0,
    selectable,
    showIcon,
    icon,
    switcherIcon,
    draggable,
    disabled,
    motion,
    loadData,
    filterTreeNode,
    height,
    itemHeight,
    keyEntities
  } = props;

  const domProps: React.HTMLAttributes<HTMLDivElement> = getDataAndAria(props);

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

        loadData,
        filterTreeNode,

        onNodeClick: onNodeClick,
        onNodeDoubleClick: onNodeDoubleClick,
        onNodeExpand: onNodeExpand,
        onNodeSelect: onNodeSelect,
        onNodeLoad: onNodeLoad,
        onNodeMouseEnter: onNodeMouseEnter,
        onNodeMouseLeave: onNodeMouseLeave,
        onNodeContextMenu: onNodeContextMenu,
        onNodeDragStart: onNodeDragStart,
        onNodeDragEnter: onNodeDragEnter,
        onNodeDragOver: onNodeDragOver,
        onNodeDragLeave: onNodeDragLeave,
        onNodeDragEnd: onNodeDragEnd,
        onNodeDrop: onNodeDrop
      }}
    >
      <div
        className={classNames(prefixCls, className, {
          [`${prefixCls}-show-line`]: showLine,
          [`${prefixCls}-focused`]: focused,
          [`${prefixCls}-active-focused`]: activeKey !== null
        })}
      >
        <NodeList
          ref={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={getActiveItem()}
          onFocus={onFocus}
          onBlur={onBlur}
          onActiveChange={onActiveChange}
          {...getTreeNodeRequiredProps()}
          {...domProps}
        />
      </div>
    </TreeContext.Provider>
  );
};

export { TreeProps };
export default Tree;
