import PropTypes, { withUndefined } from '../../_util/vue-types';
import classNames from '../../_util/classNames';
import warning from 'warning';
import { hasProp, initDefaultProps, getOptionProps, getSlot } from '../../_util/props-util';
import { cloneElement } from '../../_util/vnode';
import BaseMixin from '../../_util/BaseMixin';
import {
  convertTreeToEntities,
  convertDataToTree,
  getPosition,
  getDragNodesKeys,
  parseCheckedKeys,
  conductExpandParent,
  calcSelectedKeys,
  calcDropPosition,
  arrAdd,
  arrDel,
  posToArr,
  mapChildren,
  conductCheck,
  warnOnlyTreeNode,
  getDataAndAria,
} from './util';
import { defineComponent } from 'vue';

/**
 * Thought we still use `cloneElement` to pass `key`,
 * other props can pass with context for future refactor.
 */

function getWatch(keys = []) {
  const watch = {};
  keys.forEach(k => {
    watch[k] = {
      handler() {
        this.needSyncKeys[k] = true;
      },
      flush: 'sync',
    };
  });
  return watch;
}

const Tree = defineComponent({
  name: 'Tree',
  mixins: [BaseMixin],
  provide() {
    return {
      vcTree: this,
    };
  },
  inheritAttrs: false,
  props: initDefaultProps(
    {
      prefixCls: PropTypes.string,
      tabindex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
      children: PropTypes.any,
      treeData: PropTypes.array, // Generate treeNode by children
      showLine: PropTypes.looseBool,
      showIcon: PropTypes.looseBool,
      icon: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
      focusable: PropTypes.looseBool,
      selectable: PropTypes.looseBool,
      disabled: PropTypes.looseBool,
      multiple: PropTypes.looseBool,
      checkable: withUndefined(PropTypes.oneOfType([PropTypes.object, PropTypes.looseBool])),
      checkStrictly: PropTypes.looseBool,
      draggable: PropTypes.looseBool,
      defaultExpandParent: PropTypes.looseBool,
      autoExpandParent: PropTypes.looseBool,
      defaultExpandAll: PropTypes.looseBool,
      defaultExpandedKeys: PropTypes.array,
      expandedKeys: PropTypes.array,
      defaultCheckedKeys: PropTypes.array,
      checkedKeys: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
      defaultSelectedKeys: PropTypes.array,
      selectedKeys: PropTypes.array,
      // onClick: PropTypes.func,
      // onDoubleClick: PropTypes.func,
      // onExpand: PropTypes.func,
      // onCheck: PropTypes.func,
      // onSelect: PropTypes.func,
      loadData: PropTypes.func,
      loadedKeys: PropTypes.array,
      // 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,
      openTransitionName: PropTypes.string,
      openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
      switcherIcon: PropTypes.any,
      __propsSymbol__: PropTypes.any,
    },
    {
      prefixCls: 'rc-tree',
      showLine: false,
      showIcon: true,
      selectable: true,
      multiple: false,
      checkable: false,
      disabled: false,
      checkStrictly: false,
      draggable: false,
      defaultExpandParent: true,
      autoExpandParent: false,
      defaultExpandAll: false,
      defaultExpandedKeys: [],
      defaultCheckedKeys: [],
      defaultSelectedKeys: [],
    },
  ),

  data() {
    warning(this.$props.__propsSymbol__, 'must pass __propsSymbol__');
    warning(this.$props.children, 'please use children prop replace slots.default');
    this.needSyncKeys = {};
    this.domTreeNodes = {};
    const state = {
      _posEntities: new Map(),
      _keyEntities: new Map(),
      _expandedKeys: [],
      _selectedKeys: [],
      _checkedKeys: [],
      _halfCheckedKeys: [],
      _loadedKeys: [],
      _loadingKeys: [],
      _treeNode: [],
      _prevProps: null,
      _dragOverNodeKey: '',
      _dropPosition: null,
      _dragNodesKeys: [],
    };
    return {
      ...state,
      ...this.getDerivedState(getOptionProps(this), state),
    };
  },

  watch: {
    // watch 引用类型的改变
    ...getWatch([
      'treeData',
      'children',
      'expandedKeys',
      'autoExpandParent',
      'selectedKeys',
      'checkedKeys',
      'loadedKeys',
    ]),
    __propsSymbol__() {
      this.setState(this.getDerivedState(getOptionProps(this), this.$data));
      this.needSyncKeys = {};
    },
  },

  methods: {
    getDerivedState(props, prevState) {
      const { _prevProps } = prevState;
      const newState = {
        _prevProps: { ...props },
      };
      const self = this;
      function needSync(name) {
        return (!_prevProps && name in props) || (_prevProps && self.needSyncKeys[name]);
      }

      // ================== Tree Node ==================
      let treeNode = null;

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

      // Tree support filter function which will break the tree structure in the vdm.
      // We cache the treeNodes in state so that we can return the treeNode in event trigger.
      if (treeNode) {
        newState._treeNode = treeNode;

        // Calculate the entities data for quick match
        const entitiesMap = convertTreeToEntities(treeNode);
        newState._keyEntities = 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) {
        newState._expandedKeys = [...keyEntities.keys()];
      } else if (!_prevProps && props.defaultExpandedKeys) {
        newState._expandedKeys =
          props.autoExpandParent || props.defaultExpandParent
            ? conductExpandParent(props.defaultExpandedKeys, keyEntities)
            : props.defaultExpandedKeys;
      }

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

      // ================= checkedKeys =================
      if (props.checkable) {
        let checkedKeyEntity;

        if (needSync('checkedKeys')) {
          checkedKeyEntity = parseCheckedKeys(props.checkedKeys) || {};
        } else if (!_prevProps && props.defaultCheckedKeys) {
          checkedKeyEntity = parseCheckedKeys(props.defaultCheckedKeys) || {};
        } else if (treeNode) {
          // If treeNode changed, we also need check it
          checkedKeyEntity = parseCheckedKeys(props.checkedKeys) || {
            checkedKeys: prevState._checkedKeys,
            halfCheckedKeys: prevState._halfCheckedKeys,
          };
        }

        if (checkedKeyEntity) {
          let { checkedKeys = [], halfCheckedKeys = [] } = checkedKeyEntity;

          if (!props.checkStrictly) {
            const conductKeys = conductCheck(checkedKeys, true, keyEntities);
            ({ checkedKeys, halfCheckedKeys } = conductKeys);
          }

          newState._checkedKeys = checkedKeys;
          newState._halfCheckedKeys = halfCheckedKeys;
        }
      }
      // ================= loadedKeys ==================
      if (needSync('loadedKeys')) {
        newState._loadedKeys = props.loadedKeys;
      }

      return newState;
    },
    onNodeDragStart(event, node) {
      const { _expandedKeys } = this.$data;
      const { eventKey } = node;
      const children = getSlot(node);
      this.dragNode = node;

      this.setState({
        _dragNodesKeys: getDragNodesKeys(
          typeof children === 'function' ? children() : children,
          node,
        ),
        _expandedKeys: arrDel(_expandedKeys, eventKey),
      });
      this.__emit('dragstart', { event, node });
    },

    /**
     * [Legacy] 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, node) {
      const { _expandedKeys: expandedKeys } = this.$data;
      const { pos, eventKey } = node;

      if (!this.dragNode || !node.selectHandle) return;

      const dropPosition = calcDropPosition(event, node);

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

      // Ref: https://github.com/react-component/tree/issues/132
      // Add timeout to let onDragLevel fire before onDragEnter,
      // so that we can clean drag props for onDragLeave node.
      // Macro task for this:
      // https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-script
      setTimeout(() => {
        // Update drag over node
        this.setState({
          _dragOverNodeKey: eventKey,
          _dropPosition: dropPosition,
        });

        // Side effect for delay drag
        if (!this.delayedDragEnterLogic) {
          this.delayedDragEnterLogic = {};
        }
        Object.keys(this.delayedDragEnterLogic).forEach(key => {
          clearTimeout(this.delayedDragEnterLogic[key]);
        });
        this.delayedDragEnterLogic[pos] = setTimeout(() => {
          const newExpandedKeys = arrAdd(expandedKeys, eventKey);
          if (!hasProp(this, 'expandedKeys')) {
            this.setState({
              _expandedKeys: newExpandedKeys,
            });
          }
          this.__emit('dragenter', { event, node, expandedKeys: newExpandedKeys });
        }, 400);
      }, 0);
    },
    onNodeDragOver(event, node) {
      const { eventKey } = node;
      const { _dragOverNodeKey, _dropPosition } = this.$data;
      // Update drag position
      if (this.dragNode && eventKey === _dragOverNodeKey && node.selectHandle) {
        const dropPosition = calcDropPosition(event, node);

        if (dropPosition === _dropPosition) return;

        this.setState({
          _dropPosition: dropPosition,
        });
      }
      this.__emit('dragover', { event, node });
    },
    onNodeDragLeave(event, node) {
      this.setState({
        _dragOverNodeKey: '',
      });
      this.__emit('dragleave', { event, node });
    },
    onNodeDragEnd(event, node) {
      this.setState({
        _dragOverNodeKey: '',
      });
      this.__emit('dragend', { event, node });
      this.dragNode = null;
    },
    onNodeDrop(event, node) {
      const { _dragNodesKeys = [], _dropPosition } = this.$data;

      const { eventKey, pos } = node;

      this.setState({
        _dragOverNodeKey: '',
      });

      if (_dragNodesKeys.indexOf(eventKey) !== -1) {
        warning(false, "Can not drop to dragNode(include it's children node)");
        return;
      }

      const posArr = posToArr(pos);

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

      if (_dropPosition !== 0) {
        dropResult.dropToGap = true;
      }
      this.__emit('drop', dropResult);
      this.dragNode = null;
    },

    onNodeClick(e, treeNode) {
      this.__emit('click', e, treeNode);
    },

    onNodeDoubleClick(e, treeNode) {
      this.__emit('dblclick', e, treeNode);
    },

    onNodeSelect(e, treeNode) {
      let { _selectedKeys: selectedKeys } = this.$data;
      const { _keyEntities: keyEntities } = this.$data;
      const { multiple } = this.$props;
      const { selected, eventKey } = getOptionProps(treeNode);
      const targetSelected = !selected;
      // Update selected keys
      if (!targetSelected) {
        selectedKeys = arrDel(selectedKeys, eventKey);
      } else if (!multiple) {
        selectedKeys = [eventKey];
      } else {
        selectedKeys = arrAdd(selectedKeys, eventKey);
      }

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

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

      this.setUncontrolledState({ _selectedKeys: selectedKeys });

      const eventObj = {
        event: 'select',
        selected: targetSelected,
        node: treeNode,
        selectedNodes,
        nativeEvent: e,
      };
      this.__emit('select', selectedKeys, eventObj);
    },
    onNodeCheck(e, treeNode, checked) {
      const {
        _keyEntities: keyEntities,
        _checkedKeys: oriCheckedKeys,
        _halfCheckedKeys: oriHalfCheckedKeys,
      } = this.$data;
      const { checkStrictly } = this.$props;
      const { eventKey } = getOptionProps(treeNode);

      // Prepare trigger arguments
      let checkedObj;
      const eventObj = {
        event: 'check',
        node: treeNode,
        checked,
        nativeEvent: e,
      };

      if (checkStrictly) {
        const checkedKeys = checked
          ? arrAdd(oriCheckedKeys, eventKey)
          : arrDel(oriCheckedKeys, eventKey);
        const halfCheckedKeys = arrDel(oriHalfCheckedKeys, eventKey);
        checkedObj = { checked: checkedKeys, halfChecked: halfCheckedKeys };

        eventObj.checkedNodes = checkedKeys
          .map(key => keyEntities.get(key))
          .filter(entity => entity)
          .map(entity => entity.node);

        this.setUncontrolledState({ _checkedKeys: checkedKeys });
      } else {
        const { checkedKeys, halfCheckedKeys } = conductCheck([eventKey], checked, keyEntities, {
          checkedKeys: oriCheckedKeys,
          halfCheckedKeys: oriHalfCheckedKeys,
        });

        checkedObj = checkedKeys;

        // [Legacy] This is used for `rc-tree-select`
        eventObj.checkedNodes = [];
        eventObj.checkedNodesPositions = [];
        eventObj.halfCheckedKeys = halfCheckedKeys;

        checkedKeys.forEach(key => {
          const entity = keyEntities.get(key);
          if (!entity) return;

          const { node, pos } = entity;

          eventObj.checkedNodes.push(node);
          eventObj.checkedNodesPositions.push({ node, pos });
        });

        this.setUncontrolledState({
          _checkedKeys: checkedKeys,
          _halfCheckedKeys: halfCheckedKeys,
        });
      }
      this.__emit('check', checkedObj, eventObj);
    },
    onNodeLoad(treeNode) {
      return new Promise(resolve => {
        // We need to get the latest state of loading/loaded keys
        this.setState(({ _loadedKeys: loadedKeys = [], _loadingKeys: loadingKeys = [] }) => {
          const { loadData } = this.$props;
          const { eventKey } = getOptionProps(treeNode);

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

          // Process load data
          const promise = loadData(treeNode);
          promise.then(() => {
            const { _loadedKeys: currentLoadedKeys, _loadingKeys: currentLoadingKeys } = this.$data;
            const newLoadedKeys = arrAdd(currentLoadedKeys, eventKey);
            const newLoadingKeys = arrDel(currentLoadingKeys, eventKey);

            // onLoad should trigger before internal setState to avoid `loadData` trigger twice.
            // https://github.com/ant-design/ant-design/issues/12464
            this.__emit('load', newLoadedKeys, {
              event: 'load',
              node: treeNode,
            });
            this.setUncontrolledState({
              _loadedKeys: newLoadedKeys,
            });
            this.setState({
              _loadingKeys: newLoadingKeys,
            });
            resolve();
          });

          return {
            _loadingKeys: arrAdd(loadingKeys, eventKey),
          };
        });
      });
    },

    onNodeExpand(e, treeNode) {
      let { _expandedKeys: expandedKeys } = this.$data;
      const { loadData } = this.$props;
      const { eventKey, expanded } = getOptionProps(treeNode);

      // Update selected keys
      const index = expandedKeys.indexOf(eventKey);
      const targetExpanded = !expanded;

      warning(
        (expanded && index !== -1) || (!expanded && index === -1),
        'Expand state not sync with index check',
      );

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

      this.setUncontrolledState({ _expandedKeys: expandedKeys });
      this.__emit('expand', expandedKeys, {
        node: treeNode,
        expanded: targetExpanded,
        nativeEvent: e,
      });

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

      return null;
    },

    onNodeMouseEnter(event, node) {
      this.__emit('mouseenter', { event, node });
    },

    onNodeMouseLeave(event, node) {
      this.__emit('mouseleave', { event, node });
    },

    onNodeContextMenu(event, node) {
      event.preventDefault();
      this.__emit('rightClick', { event, node });
    },

    /**
     * Only update the value which is not in props
     */
    setUncontrolledState(state) {
      let needSync = false;
      const newState = {};
      const props = getOptionProps(this);
      Object.keys(state).forEach(name => {
        if (name.replace('_', '') in props) return;
        needSync = true;
        newState[name] = state[name];
      });

      if (needSync) {
        this.setState(newState);
      }
    },

    registerTreeNode(key, node) {
      if (node) {
        this.domTreeNodes[key] = node;
      } else {
        delete this.domTreeNodes[key];
      }
    },

    isKeyChecked(key) {
      const { _checkedKeys: checkedKeys = [] } = this.$data;
      return checkedKeys.indexOf(key) !== -1;
    },

    /**
     * [Legacy] Original logic use `key` as tracking clue.
     * We have to use `cloneElement` to pass `key`.
     */
    renderTreeNode(child, index, level = 0) {
      const {
        _keyEntities: keyEntities,
        _expandedKeys: expandedKeys = [],
        _selectedKeys: selectedKeys = [],
        _halfCheckedKeys: halfCheckedKeys = [],
        _loadedKeys: loadedKeys = [],
        _loadingKeys: loadingKeys = [],
        _dragOverNodeKey: dragOverNodeKey,
        _dropPosition: dropPosition,
      } = this.$data;
      const pos = getPosition(level, index);
      let key = child.key;
      if (!key && (key === undefined || key === null)) {
        key = pos;
      }
      if (!keyEntities.get(key)) {
        warnOnlyTreeNode();
        return null;
      }

      return cloneElement(child, {
        eventKey: key,
        expanded: expandedKeys.indexOf(key) !== -1,
        selected: selectedKeys.indexOf(key) !== -1,
        loaded: loadedKeys.indexOf(key) !== -1,
        loading: loadingKeys.indexOf(key) !== -1,
        checked: this.isKeyChecked(key),
        halfChecked: halfCheckedKeys.indexOf(key) !== -1,
        pos,

        // [Legacy] Drag props
        dragOver: dragOverNodeKey === key && dropPosition === 0,
        dragOverGapTop: dragOverNodeKey === key && dropPosition === -1,
        dragOverGapBottom: dragOverNodeKey === key && dropPosition === 1,
        key,
      });
    },
  },

  render() {
    const { _treeNode: treeNode } = this.$data;
    const { prefixCls, focusable, showLine, tabindex = 0 } = this.$props;
    const domProps = getDataAndAria({ ...this.$props, ...this.$attrs });
    const { class: className, style } = this.$attrs;
    return (
      <ul
        {...domProps}
        class={classNames(prefixCls, className, {
          [`${prefixCls}-show-line`]: showLine,
        })}
        style={style}
        role="tree"
        unselectable="on"
        tabindex={focusable ? tabindex : null}
      >
        {mapChildren(treeNode, (node, index) => this.renderTreeNode(node, index))}
      </ul>
    );
  },
});

export { Tree };

export default Tree;