/**
 * Copyright (c) 2022 - present TinyVue Authors.
 * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd.
 *
 * Use of this source code is governed by an MIT-style license.
 *
 * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
 * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
 * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
 *
 */

import { hasOwn, isNull } from '../type'
import { getNodeKey } from './util'
import { Node } from './node'

interface TreeStoreOptions {
  data: any
  key?: string
  props?: Record<string, any>
  load?: (node: Node, callback: (data: any) => void) => void
  lazy?: boolean
  checkStrictly?: boolean
  defaultCheckedKeys?: any[]
  defaultExpandedKeys?: any[]
  autoExpandParent?: boolean
  afterLoad?: (params: { data: any; init: boolean }) => void
  filterNodeMethod?: (value: any, data: any, node: Node) => boolean
  [key: string]: any
}

// 为Node类型声明安全类型
type NodeType = any
type FilterNodeMethodType = (value: any, data: any, node: NodeType) => boolean

export class TreeStore {
  currentNode: NodeType | null = null
  currentNodeKey: any = null
  nodesMap: Record<string, NodeType> = {}
  root: NodeType
  data: any
  lazy = false
  load?: (node: NodeType, callback: (data: any) => void) => void
  afterLoad?: (params: { data: any; init: boolean }) => void
  props: Record<string, any> = {}
  filterNodeMethod?: FilterNodeMethodType
  key = ''
  defaultCheckedKeys: any[] = []
  checkStrictly = false
  defaultExpandedKeys: any[] = []
  autoExpandParent = true

  constructor(options: TreeStoreOptions) {
    this.currentNode = null
    this.currentNodeKey = null

    for (let option in options) {
      if (hasOwn.call(options, option)) {
        this[option] = options[option]
      }
    }

    this.nodesMap = {}

    this.root = new Node({ data: this.data, store: this })

    if (this.lazy && this.load) {
      this.load(this.root, (data) => {
        this.root.doCreateChildren(data)
        this._initDefaultCheckedNodes()
        typeof this.afterLoad === 'function' && this.afterLoad({ data, init: true })
      })
    } else {
      this._initDefaultCheckedNodes()
    }
  }

  getMappingData(data: any) {
    const props = this.props || {}
    const mapping = {}

    for (let key in props) {
      if (hasOwn.call(props, key)) {
        mapping[key] = data[props[key]]
      }
    }

    return { ...data, ...mapping }
  }

  filter(value: any) {
    const { lazy, filterNodeMethod, getMappingData } = this

    const walkTree = (node: NodeType) => {
      const childNodes = node.root ? node.root.childNodes : node.childNodes

      childNodes.forEach((child) => {
        // 筛选时需要添加mapping字段,但是不能修改用户的数据
        const mappingData = getMappingData.call(this, child.data)
        if (filterNodeMethod) {
          child.visible = filterNodeMethod.call(child, value, mappingData, child)
        }

        walkTree(child)
      })

      if (!node.visible && childNodes.length) {
        let allHidden = !childNodes.some(({ visible }) => visible)

        if (node.root) {
          node.root.visible = allHidden === false
        } else {
          node.visible = allHidden === false
        }
      }

      if (!value) {
        return
      }
      if (node.visible && !node.isLeaf && !lazy) {
        node.expand(null, this.autoExpandParent)
      }
    }

    walkTree(this)
  }

  setData(newVal: any) {
    if (newVal !== this.root.data) {
      this.root.setData(newVal)
      this._initDefaultCheckedNodes()
    } else {
      this.root.updateChildren()
    }
  }

  getNode(data: any): NodeType | null {
    if (data instanceof Node) {
      return data
    }

    const nodeKey = typeof data !== 'object' ? data : getNodeKey(this.key, data)

    return this.nodesMap[nodeKey] || null
  }

  insertBefore(data: any, insertData: any) {
    const refNode = this.getNode(insertData)
    if (refNode && refNode.parent) {
      refNode.parent.insertBefore({ data }, refNode)
    }
  }

  insertAfter(data: any, insertData: any) {
    const refNode = this.getNode(insertData)
    if (refNode && refNode.parent) {
      refNode.parent.insertAfter({ data }, refNode)
    }
  }

  remove(data: any, isSaveChildNode?: boolean, isNode?: boolean) {
    const treeNode = isNode ? data : this.getNode(data)

    if (treeNode && treeNode.parent) {
      if (treeNode === this.currentNode) {
        this.currentNode = null
      }

      if (isSaveChildNode && treeNode.childNodes) {
        treeNode.childNodes.forEach((child) => {
          treeNode.parent.insertChild({ data: child.data })
        })
      }

      treeNode.parent.removeChild(treeNode)
    }
  }

  append(data: any, parentData?: any, index?: number) {
    const parentNode = parentData ? this.getNode(parentData) : this.root

    if (parentNode) {
      const child = parentNode.insertChild({ data }, index)
      data._isNewNode && this.registerNode(child)
    }
  }

  setDefaultCheckedKey(newValue: any[]) {
    if (newValue !== this.defaultCheckedKeys) {
      this.defaultCheckedKeys = newValue
      this._initDefaultCheckedNodes()
    }
  }

  _initDefaultCheckedNodes() {
    const defaultCheckedKeys = this.defaultCheckedKeys || []
    const nodesMap = this.nodesMap

    defaultCheckedKeys.forEach((checkedKey) => {
      const node = nodesMap[checkedKey]

      node && node.setChecked(true, !this.checkStrictly)
    })
  }

  _initDefaultCheckedNode(node: NodeType) {
    const defaultCheckedKeys = this.defaultCheckedKeys || []

    ~defaultCheckedKeys.indexOf(node.key) && node.setChecked(true, !this.checkStrictly)
  }

  getCheckedKeys(leafOnly = false) {
    return this.getCheckedNodes(leafOnly).map((node) => (node || {})[this.key])
  }

  getHalfCheckedKeys() {
    return this.getHalfCheckedNodes().map((node) => (node || {})[this.key])
  }

  deregisterNode(node: NodeType) {
    const key = this.key
    if (!key || !node || !node.data) {
      return
    }

    node.childNodes.forEach((child) => {
      this.deregisterNode(child)
    })

    delete this.nodesMap[node.key]
  }

  registerNode(node: NodeType) {
    const key = this.key
    if (!key || !node || !node.data) {
      return
    }

    const nodeKey = node.key
    if (nodeKey !== undefined) {
      this.nodesMap[nodeKey] = node
    }
  }

  getCheckedNodes(leafOnly = false, includeHalfChecked = false, isNode = false): any[] {
    const checkedNodes: any[] = []

    const walkTree = (node: NodeType) => {
      const childNodes = node.root ? node.root.childNodes : node.childNodes

      childNodes.forEach((child) => {
        const { checked, indeterminate, isLeaf, data } = child

        if ((checked || (includeHalfChecked && indeterminate)) && (!leafOnly || (leafOnly && isLeaf))) {
          checkedNodes.push(isNode ? child : data)
        }

        walkTree(child)
      })
    }

    walkTree(this)

    return checkedNodes
  }

  getHalfCheckedNodes(): any[] {
    const nodes: any[] = []

    const walkTree = (node: NodeType) => {
      const childNodes = node.root ? node.root.childNodes : node.childNodes

      childNodes.forEach((child) => {
        const { indeterminate, data } = child

        indeterminate && nodes.push(data)

        walkTree(child)
      })
    }

    walkTree(this)

    return nodes
  }

  _getAllNodes(): NodeType[] {
    const allNodes: NodeType[] = []
    const nodesMap = this.nodesMap

    Object.keys(nodesMap).forEach((nodeKey) => {
      hasOwn.call(nodesMap, nodeKey) && allNodes.push(nodesMap[nodeKey])
    })

    return allNodes
  }

  updateChildren(key: string, data: any[]) {
    const node = this.nodesMap[key]
    if (!node) {
      return
    }

    const childNodes = node.childNodes

    for (let i = childNodes.length - 1; i >= 0; i--) {
      this.remove(childNodes[i].data, false, false)
    }

    for (let i = 0, len = data.length; i < len; i++) {
      const child = data[i]
      this.append(child, node.data)
    }
  }

  _setCheckedKeys(key: string, leafOnly = false, checkedKeys = {}) {
    const nodes = this._getAllNodes().sort((prevNode, nextNode) => nextNode.level - prevNode.level)
    const cache = Object.create(null)
    const keys = Object.keys(checkedKeys)

    nodes.forEach((node) => {
      node.setChecked(false, false)
    })

    for (let i = 0, len = nodes.length; i < len; i++) {
      const node = nodes[i]
      const nodeKey = node.data[key].toString()
      let checked = ~keys.indexOf(nodeKey)

      if (!checked) {
        if (node.checked && !cache[nodeKey]) {
          node.setChecked(false, false)
        }
      } else {
        let parentNode = node.parent

        while (parentNode && parentNode.level > 0) {
          cache[parentNode.data[key]] = true
          parentNode = parentNode.parent
        }

        if (node.isLeaf || this.checkStrictly) {
          node.setChecked(true, false)
        } else if (leafOnly) {
          node.setChecked(false, false)

          const walkTree = (node: NodeType) => {
            const childNodes = node.childNodes

            childNodes.forEach((child) => {
              !child.isLeaf && child.setChecked(false, false)

              walkTree(child)
            })
          }

          walkTree(node)
        } else {
          node.setChecked(true, true)
        }
      }
    }
  }

  setDefaultExpandedKeys(keys: any[]) {
    keys = keys || []
    this.defaultExpandedKeys = keys

    keys.forEach((key) => {
      const node = this.getNode(key)
      node && node.expand(null, this.autoExpandParent)
    })
  }

  setCheckedKeys(keys: any[], leafOnly = false) {
    this.defaultCheckedKeys = keys
    const checkedKeys = {}

    keys.forEach((key) => {
      checkedKeys[key] = true
    })

    this._setCheckedKeys(this.key, leafOnly, checkedKeys)
  }

  setCheckedNodes(array: any[], leafOnly = false) {
    const key = this.key
    const checkedKeys = {}

    array.forEach((item) => {
      checkedKeys[(item || {})[key]] = true
    })

    this._setCheckedKeys(key, leafOnly, checkedKeys)
  }

  setChecked(data: any, checked: boolean, deep: boolean) {
    const node = this.getNode(data)

    node && node.setChecked(!!checked, deep)
  }

  setCurrentNode(currentNode: NodeType) {
    const prevNode = this.currentNode

    if (prevNode) {
      prevNode.isCurrent = false
    }

    this.currentNode = currentNode

    if (currentNode) {
      this.currentNode.isCurrent = true
    }
  }

  getCurrentNode() {
    return this.currentNode
  }

  setCurrentNodeKey(key: any) {
    if (isNull(key)) {
      this.currentNode && (this.currentNode.isCurrent = false)
      this.currentNode = null

      return
    }

    const node = this.getNode(key)

    node && this.setCurrentNode(node)
  }

  setUserCurrentNode(node: any) {
    const key = node[this.key]
    const currNode = this.nodesMap[key]

    this.setCurrentNode(currNode)
  }

  getData(data: any) {
    return (this.getNode(data) || {}).data
  }

  getAllData() {
    const children = this.props.children
    const walkTree = (nodes: NodeType[]) => {
      return nodes.map((node) => {
        return { ...node.data, [children]: walkTree(node.childNodes) }
      })
    }

    return walkTree(this.root.childNodes)
  }
}