const checkLeaf = (item, isLeaf, hasChildren) => item[isLeaf] || !hasChildren;

const getNode = (
  { selectedValue, nodeMap, dataFormat },
  originNodeData,
  { level, parentKey }
) => {
  let item = Object.assign({}, originNodeData);

  const { value, children, hasChildren, isLeaf, disabled } = dataFormat;
  const nodeKey = item[value];
  const nodeChildren = Array.isArray(item[children]) ? item[children] : [];
  const nodeHasChildren = item[hasChildren] || nodeChildren.length;
  const nodeIsLeaf = checkLeaf(item, isLeaf, nodeHasChildren);

  item.level = level;
  item.isRoot = !level;
  item.isLeaf = nodeIsLeaf;
  item.expanded = false;
  item.selected = !Array.isArray(selectedValue) && nodeKey === selectedValue;
  item.checked =
    Array.isArray(selectedValue) && selectedValue.includes(nodeKey);
  item.parentKey = parentKey;
  item.disabled = item[disabled];

  if (!nodeChildren.length) {
    item[children] = [];
  }

  if (!nodeIsLeaf) {
    item.indeterminate = false;
  }

  if (!nodeMap.has(nodeKey)) {
    nodeMap.set(nodeKey, item);
  }

  return item;
};

let selectedNodes = [];
let parentKeys = [];

class MdcTree {
  constructor(treeData) {
    this.treeData = treeData;
  }

  getData(nodes, level = 0, parentKey = '') {
    const { dataFormat, maxLevel } = this.treeData;

    const list = [];
    const { value, children, hasChildren } = dataFormat;

    for (let i = 0, len = nodes.length; i < len; i++) {
      let item = getNode(this.treeData, nodes[i], {
        level,
        parentKey
      });

      const nodeChildren = Array.isArray(item[children]) ? item[children] : [];
      const nodeHasChildren = item[hasChildren] || nodeChildren.length;

      if (level < maxLevel && nodeHasChildren) {
        item[children] = this.getData(nodeChildren, level + 1, item[value]);
      }

      list.push(item);
    }

    return list;
  }

  /** For tree node */
  static addData(treeData, item, nodes) {
    const { dataFormat, nodeMap } = treeData;

    const list = [];
    const { value, children } = dataFormat;
    const level = item.level + 1;
    const parentKey = item[value];

    for (let i = 0, len = nodes.length; i < len; i++) {
      let subitem = getNode(treeData, nodes[i], {
        level,
        parentKey
      });

      if (subitem.checked) {
        this.setMultipleSelectedValue(treeData, subitem[value], true);
      }

      list.push(subitem);
    }

    item[children] = list;
    item.expanded = true;

    nodeMap.set(parentKey, item);
  }

  static async onExpand(treeData, item) {
    if (treeData.loadData) {
      const { dataFormat } = treeData;
      const hasChildren =
        item[dataFormat.children] && item[dataFormat.children].length;

      if (hasChildren) {
        item.expanded = !item.expanded;
      } else {
        const nodes = await treeData.loadData(item[dataFormat.value], item);
        if (Array.isArray(nodes)) {
          this.addData(treeData, item, nodes);
          // review parent node
          const allChecked = nodes.every((node) =>
            treeData.selectedValue.includes(node[dataFormat.value])
          );
          const hasChecked = allChecked
            ? true
            : nodes.some((node) =>
                treeData.selectedValue.includes(node[dataFormat.value])
              );
          if (hasChecked) {
            if (allChecked || nodes.length === 1) {
              const parentNodeKey = nodes[0][dataFormat.parentKey];
              if (parentNodeKey) {
                treeData.selectedValue.push(parentNodeKey);
                item.checked = true;
              } else {
                console.warn('[UiTree]', 'Missing `parentKey`');
              }
            } else {
              item.indeterminate = true;
            }
          }
        } else {
          console.warn('[UiTree]', 'Invalid data');
        }
      }
    } else {
      item.expanded = !item.expanded;
    }
  }

  static async collapseAllNode(treeData, nodes) {
    const { dataFormat, nodeMap } = treeData;

    for await (let node of nodes) {
      const nodeKey = node[dataFormat.value];
      const item = nodeMap.get(nodeKey);

      item.expanded = false;
      if (item.children && item.children.length) {
        this.collapseAllNode(treeData, item.children);
      }
    }
    return true;
  }

  /** For single tree **/

  static setSingleSelectedValue(treeData, nodeKey, selected) {
    const { nodeMap } = treeData;
    const item = nodeMap.get(nodeKey);

    if (item) {
      item.selected = selected;

      treeData.selectedEvent = {
        selected,
        selectedNodes: nodeKey,
        node: item
      };
    }
  }

  static onSelect(treeData, item) {
    const { dataFormat, selectedValue } = treeData;
    const nodeKey = item[dataFormat.value];

    if (selectedValue) {
      this.setSingleSelectedValue(treeData, selectedValue, false);
    }

    treeData.selectedValue = nodeKey;
    this.setSingleSelectedValue(treeData, nodeKey, true);
  }

  /** For multiple tree **/

  static setMultipleSelectedValue(treeData, currentNodeKey, checked) {
    const { dataFormat, nodeMap, filterParentNode } = treeData;
    const item = nodeMap.get(currentNodeKey);

    if (checked && !item.indeterminate) {
      if (!treeData.selectedValue.includes(currentNodeKey)) {
        if (filterParentNode) {
          item.isLeaf && treeData.selectedValue.push(currentNodeKey);
        } else {
          treeData.selectedValue.push(currentNodeKey);
        }
      }
    } else {
      treeData.selectedValue = treeData.selectedValue.filter(
        (value) => value !== currentNodeKey
      );
    }
  }

  static setChildrenCheckedValue(treeData, nodes, checked) {
    const { dataFormat, nodeMap } = treeData;
    const { value, children } = dataFormat;

    for (let i = 0, len = nodes.length; i < len; i++) {
      let item = Object.assign({}, nodes[i]);

      const nodeKey = item[value];
      const nodeChildren = item[children];
      const subitem = nodeMap.get(nodeKey);

      if (subitem) {
        if (checked) {
          !subitem.checked && selectedNodes.push(nodeKey);
        } else {
          subitem.checked && selectedNodes.push(nodeKey);
        }

        subitem.indeterminate = false;
        subitem.checked = checked;
        this.setMultipleSelectedValue(treeData, nodeKey, checked);
      }

      if (!item.isLeaf && nodeChildren.length) {
        this.setChildrenCheckedValue(treeData, nodeChildren, checked);
      }
    }
  }

  static setParentCheckedValue(treeData, item) {
    const { dataFormat, nodeMap } = treeData;
    const { value, children } = dataFormat;

    if (item) {
      const nodeKey = item[value];
      const nodeChildren = item[children];
      const nodeCheckedList = nodeChildren.filter(
        (subitem) => subitem.checked || subitem.indeterminate
      );
      const subitem = nodeMap.get(nodeKey);

      if (nodeCheckedList.length) {
        const checkedAllList = nodeCheckedList.filter(
          (subitem) => subitem.checked
        ).length;
        const checkedAll = checkedAllList === nodeChildren.length;

        if (checkedAll) {
          !subitem.checked && selectedNodes.push(nodeKey);
        } else {
          subitem.checked && selectedNodes.push(nodeKey);
        }

        subitem.checked = checkedAll;
        subitem.indeterminate = !checkedAll;
        this.setMultipleSelectedValue(treeData, nodeKey, checkedAll);
      } else {
        subitem.checked = false;
        subitem.indeterminate = false;
        this.setMultipleSelectedValue(treeData, nodeKey, subitem.checked);
      }

      if (!item.isRoot) {
        this.setParentCheckedValue(treeData, nodeMap.get(item.parentKey));
      }
    }
  }

  static onCheck(treeData, item, forceChecked = null) {
    let checked = !item.checked;
    if (typeof forceChecked === 'boolean') {
      checked = forceChecked;
    }

    const { dataFormat, nodeMap, singleChecked } = treeData;
    const { value, children } = dataFormat;
    const nodeKey = item[value];
    const nodeChildren = item[children];

    if (singleChecked) {
      item.checked = checked;
      this.setMultipleSelectedValue(treeData, nodeKey, checked);

      treeData.selectedEvent = {
        checked,
        checkedNodes: [nodeKey],
        node: item
      };
    } else {
      selectedNodes = [nodeKey];

      if (item.isLeaf) {
        item.checked = checked;
        this.setMultipleSelectedValue(treeData, nodeKey, checked);
      } else {
        if (item.indeterminate) {
          item.indeterminate = false;
          checked = true;
        }

        item.checked = checked;
        this.setMultipleSelectedValue(treeData, nodeKey, checked);
        this.setChildrenCheckedValue(treeData, nodeChildren, checked);
      }

      if (!item.isRoot) {
        this.setParentCheckedValue(treeData, nodeMap.get(item.parentKey));
      }

      treeData.selectedEvent = {
        checked,
        checkedNodes: selectedNodes,
        node: item
      };
    }
  }

  /** For expanding **/

  static async handleExpandKeys(treeData, nodes, defaultExpandedKeys) {
    const { dataFormat, nodeMap } = treeData;

    for await (let node of nodes) {
      const nodeKey = node[dataFormat.value];
      const item = nodeMap.get(nodeKey);

      defaultExpandedKeys.includes(nodeKey) && this.onExpand(treeData, item);
      if (node.children && node.children.length) {
        this.handleExpandKeys(treeData, node.children, defaultExpandedKeys);
      }
    }
  }

  static async handleExpandAll(treeData, nodes) {
    const { dataFormat, nodeMap } = treeData;

    for await (let node of nodes) {
      const nodeKey = node[dataFormat.value];
      const item = nodeMap.get(nodeKey);

      this.onExpand(treeData, item);
      if (item.children && item.children.length) {
        this.handleExpandAll(treeData, item.children);
      }
    }
  }

  static async findTreeNode(tree, key, value) {
    if (tree[key] === value) return tree;

    if (tree.children && tree.children.length) {
      for (let i = 0; i < tree.children.length; i++) {
        const node = await this.findTreeNode(tree.children[i], key, value);
        if (node !== null) return node;
      }
    }
    return null;
  }

  static toReverseArray(arr) {
    const newArr = [];
    for (let i = arr.length - 1; i >= 0; i--) {
      newArr.push(arr[i]);
    }
    return newArr;
  }

  static async handleAutoExpandSelected(
    nodeList,
    key,
    selectedValue,
    treeData
  ) {
    const allNodeCollapsed = await this.collapseAllNode(treeData, nodeList);
    if (allNodeCollapsed) {
      const result = await this.findTreeNode(nodeList[0], key, selectedValue);
      parentKeys.push(result[key]);
      if (result.parentKey) {
        this.handleAutoExpandSelected(
          nodeList,
          key,
          result.parentKey,
          treeData
        );
      }

      if (!result.parentKey) {
        const reversedArr = this.toReverseArray(parentKeys);
        treeData && this.handleExpandKeys(treeData, nodeList, reversedArr);
      }
    }
  }

  /** For init tree **/

  static async setExpanded(
    treeData,
    nodeList,
    { autoExpandParent, defaultExpandedKeys, autoExpandAll }
  ) {
    const { dataFormat, nodeMap } = treeData;

    if (autoExpandAll) {
      this.handleExpandAll(treeData, nodeList);
    }

    if (autoExpandParent) {
      if (defaultExpandedKeys.length) {
        this.handleExpandKeys(treeData, nodeList, defaultExpandedKeys);
      } else {
        for await (let node of nodeList) {
          const nodeKey = node[dataFormat.value];
          const item = nodeMap.get(nodeKey);

          this.onExpand(treeData, item);
        }
      }
    }
  }

  static resetSelected(treeData, oldSelectedKeys) {
    const { nodeMap } = treeData;

    for (let i = 0, len = oldSelectedKeys.length; i < len; i++) {
      const nodeKey = oldSelectedKeys[i];
      const item = nodeMap.get(nodeKey);
      if (item) {
        this.onCheck(treeData, item, false);
      }
    }
  }

  static setSelected(
    treeData,
    newSelectedKeys,
    { nodeList, autoExpandSelected }
  ) {
    const { dataFormat, nodeMap, multiple } = treeData;

    const selectedKeys = Array.isArray(newSelectedKeys)
      ? newSelectedKeys
      : [newSelectedKeys];
    for (let i = 0, len = selectedKeys.length; i < len; i++) {
      const nodeKey = selectedKeys[i];
      const item = nodeMap.get(nodeKey);
      if (item) {
        multiple
          ? this.onCheck(treeData, item, true)
          : this.onSelect(treeData, item);
      }
    }

    if (autoExpandSelected && !Array.isArray(newSelectedKeys)) {
      parentKeys = [];
      this.handleAutoExpandSelected(
        nodeList,
        dataFormat.value,
        newSelectedKeys,
        treeData
      );
    }
  }

  /** For tree operation **/
  static async createNode(treeData, parentKey, originNodeData) {
    const { dataFormat, nodeMap } = treeData;
    const { value, children, hasChildren } = dataFormat;

    const parentItem = nodeMap.get(parentKey);
    const nodeKey = originNodeData[value];

    let item = getNode(treeData, originNodeData, {
      level: parentItem.level + 1,
      parentKey,
      checked: false
    });

    if (parentItem.isLeaf) {
      parentItem[children].unshift(item);
      if (!parentItem[hasChildren]) {
        parentItem[hasChildren] = true;
      }
      parentItem.isLeaf = false;
    } else {
      if (parentItem[hasChildren]) {
        if (parentItem[children].length) {
          parentItem[children].unshift(item);
        } else {
          await this.onExpand(treeData, parentItem);
        }
      } else {
        parentItem[children].unshift(item);
        parentItem[hasChildren] = true;
        parentItem.expanded = true;
      }
    }

    nodeMap.set(parentKey, parentItem);
    nodeMap.set(nodeKey, item);
  }

  static updateNode(treeData, parentKey, originNodeData) {
    const { dataFormat, nodeMap } = treeData;
    const { value, children } = dataFormat;

    const nodeKey = originNodeData[value];
    const item = nodeMap.get(nodeKey);
    Object.keys(item).forEach((key) => {
      if (typeof originNodeData[key] !== 'undefined') {
        item[key] = originNodeData[key];
      }
    });

    const parentItem = nodeMap.get(parentKey);
    const parentChildren = parentItem[children];
    const index = parentChildren.findIndex((item) => item[value] === nodeKey);
    parentItem[children][index] = item;

    nodeMap.set(parentKey, parentItem);
    nodeMap.set(nodeKey, item);
  }

  static deleteNode(treeData, parentKey, originNodeData) {
    const { dataFormat, nodeMap } = treeData;
    const { value, children, hasChildren } = dataFormat;

    const nodeKey = originNodeData[value];
    if (nodeMap.has(nodeKey)) {
      const parentItem = nodeMap.get(parentKey);
      const parentChildren = parentItem[children];

      parentChildren.splice(
        parentChildren.findIndex((item) => item[value] === nodeKey),
        1
      );
      parentItem[hasChildren] = parentChildren.length;
      if (!parentItem[hasChildren]) {
        parentItem.isLeaf = true;
        parentItem.expanded = false;
      }

      nodeMap.set(parentKey, parentItem);
      nodeMap.delete(nodeKey);
    }
  }
}

export { MdcTree };
