<template>
  <div
    :class="['draggable-tree', 'border-transparent', { 'hover-border-color': hoveringNodeId === rootId }]"
    @dragover="handleContainerDragOver"
    @dragenter="handleContainerDragOver"
    @dragleave="handleContainerDragLeave"
    @drop="handleDrop($event, { id: rootId })"
  >
    <div
      v-for="(node, rowIndex) of filteredNodesWithAncestors"
      v-show="!node.collapsed"
      :class="[
        'row',
        'border-transparent',
        {
          active: String(active) === String(node.id),
          ['hover-border-color']: hoveringNodeId === node.id
        }
      ]"
      :key="node.id"
      :draggable="draggable ? 'true' : undefined"
      @dragstart="handleDragStart($event, node)"
      @dragover="handleDragOver($event, node)"
      @dragenter="handleDragOver($event, node)"
      @drop="handleDrop($event, node)"
      @dragend="handleDragEnd"
    >
      <div class="content" @click="handleClickRow($event, node)">
        <layer-lines :line-data="layerLine[rowIndex]" :level="node.level"></layer-lines>
        <div class="prefix-icon" @click="handleSwitchCollapse(node)">
          <svg-icon v-if="node.rawData.isPage" :name="collapseMap[node.id] ? 'page-collection' : 'page'"></svg-icon>
          <svg-icon v-else :name="collapseMap[node.id] ? 'folder' : 'folder-wold'"></svg-icon>
        </div>
        <label>{{ node.label }}</label>
      </div>
      <slot name="row-suffix" :node="node"></slot>
    </div>
  </div>
</template>

<script lang="ts" setup>
/* metaService: engine.plugins.appmanage.Tree */
import { computed, defineEmits, defineProps, ref, watch } from 'vue'
import LayerLines from './LayerLines.vue'

const props = defineProps({
  data: {
    type: Object,
    required: true
  },
  idKey: {
    type: String,
    default: 'id'
  },
  labelKey: {
    type: String,
    default: 'label'
  },
  childrenKey: {
    type: String,
    default: 'children'
  },
  active: {
    type: String
  },
  filterValue: {
    type: String,
    default: ''
  },
  rootId: {
    type: [String, Number],
    default: 'root'
  },
  draggable: {
    type: Boolean,
    default: false
  }
})

const emit = defineEmits(['clickRow', 'moveNode'])

const useCollapseMap = () => {
  const collapseMap = ref<Record<string, boolean>>({})

  const setCollapse = (id: string | number, value: boolean) => {
    collapseMap.value[id] = value
  }

  const switchCollapse = (id: string | number) => {
    collapseMap.value[id] = !collapseMap.value[id]
  }

  return { collapseMap, setCollapse, switchCollapse }
}

const { collapseMap, setCollapse, switchCollapse } = useCollapseMap()

export interface TreeNode {
  id: string | number
  label: string
  parentId?: string | number
  level: number
  collapsed?: boolean
  rawData?: any
}

const handleSwitchCollapse = (node: TreeNode) => {
  const children = node.rawData[props.childrenKey]
  if (Array.isArray(children) && children.length > 0) {
    switchCollapse(node.id)
  }
}

const flattenTreeData = (node: any, parentId?: string | number, level = 0, collapsed = false): TreeNode[] => {
  const { idKey, labelKey, childrenKey } = props

  const currentNode = {
    id: node[idKey],
    label: node[labelKey],
    parentId,
    level,
    collapsed,
    rawData: node
  }
  const result: TreeNode[] = [currentNode]

  const children = node[childrenKey]

  if (Array.isArray(children)) {
    for (const child of children) {
      result.push(...flattenTreeData(child, currentNode.id, level + 1, collapsed || collapseMap.value[currentNode.id]))
    }
  }

  return result
}

const nodes = computed(() => {
  return flattenTreeData({ [props.idKey]: props.rootId, [props.childrenKey]: props.data }).slice(1)
})

const nodesMap = computed<Record<string | number, TreeNode>>(() => {
  return nodes.value.reduce((result, node) => {
    result[node.id] = node
    return result
  }, {} as Record<string | number, TreeNode>)
})

const getAncestorIds = (nodeId: string | number): (string | number)[] => {
  const currentNode = nodesMap.value[nodeId]

  if (!currentNode || !currentNode.parentId) {
    return []
  }

  const ancestors = getAncestorIds(currentNode.parentId)

  ancestors.push(currentNode.parentId)

  return ancestors
}

const filteredNodes = ref<TreeNode[]>([])

// 下面两个 watch 对应的两个重新计算 filteredNodes 的场景。场景2比场景1多了展开匹配到的节点的操作

// 场景1. 页面树的结构发生变化,或者有节点收起或者展开,重新计算 filteredNodes
watch(nodes, (nodes) => {
  filteredNodes.value = nodes.filter((node) => node.label.toLowerCase().includes(props.filterValue.toLowerCase()))
})

// 场景2. 过滤条件发生变化,重新计算 filteredNodes,并且展开匹配到的节点
watch(
  () => props.filterValue,
  (filterValue) => {
    const filtered = nodes.value.filter((node) => node.label.toLowerCase().includes(filterValue.toLowerCase()))

    let collapseMapChanged = false
    if (filterValue) {
      filtered.forEach((node) => {
        // 每个节点的祖先节点中,如果存在折叠的节点,则展开
        for (const id of getAncestorIds(node.id)) {
          if (collapseMap.value[id]) {
            setCollapse(id, false)
            collapseMapChanged = true
          }
        }
      })
    }

    // - 如果 collapseMap 有变化,会自动重新计算 nodes,更新路径:
    //   props.filterValue -> collapseMap -> nodes -> filteredNodes -> filteredNodesWithAncestors
    // - 如果 collapseMap 没有变化,则重新设置 filteredNodes,更新路径:
    //   props.filterValue -> filteredNodes -> filteredNodesWithAncestors
    if (!collapseMapChanged) {
      filteredNodes.value = filtered
    }
  }
)

const filteredNodesWithAncestors = computed(() => {
  const idSet = new Set()

  for (const node of filteredNodes.value) {
    idSet.add(node.id)
    for (const id of getAncestorIds(node.id)) {
      idSet.add(id)
    }
  }

  return nodes.value.filter((node) => idSet.has(node.id))
})

const lines = {
  node: 0b01, // └
  layer: 0b10, // │
  layerNode: 0b11 // ├
}

const layerLine = computed(() => {
  const result: Record<number, Record<number, number>> = {}

  const nodes = filteredNodesWithAncestors.value

  for (const [index, node] of nodes.entries()) {
    result[index] = result[index] || {}
    result[index][node.level - 1] = lines.node

    if (node.parentId !== props.rootId) {
      const parentIndex = nodes.findIndex((item) => item.id === node.parentId)
      for (let i = parentIndex + 1; i < index; i++) {
        result[i][node.level - 1] = (result[i][node.level - 1] || 0) | lines.layer
      }
    }
  }

  return result
})

const handleClickRow = (event: MouseEvent, node: TreeNode) => {
  // 点击事件来自折叠图标,不触发 clickRow 事件。点击事件仍然可以冒泡
  const currentTarget = event.currentTarget as HTMLElement
  if (currentTarget.querySelector('div.prefix-icon')?.contains(event.target as Node)) {
    return
  }

  emit('clickRow', node)
}

const draggedNode = ref<TreeNode | null>(null)
const hoveringNodeId = ref<string | number | null>(null)

const handleDragStart = (_event: DragEvent, node: TreeNode) => {
  if (!props.draggable) {
    return
  }

  draggedNode.value = node
}

// dragover和dragenter事件回调函数都为handleDragOver。跨行拖动时,禁止拖拽图标可能会闪一下,所以将dragenter事件也加上回调函数
const handleDragOver = (event: DragEvent, node: TreeNode) => {
  if (!props.draggable) {
    return
  }

  const isDescendant = getAncestorIds(node.id).includes(draggedNode.value!.id)

  if (!isDescendant) {
    // 阻止默认行为以允许放置
    event.preventDefault()
    hoveringNodeId.value = node.id
  } else {
    hoveringNodeId.value = null
  }
}

const handleDrop = (event: DragEvent, node: Pick<TreeNode, 'id'>) => {
  event.preventDefault()

  const dragged = draggedNode.value
  draggedNode.value = null

  if (!dragged) {
    return
  }

  const isDescendant = getAncestorIds(node.id).includes(dragged.id)

  if (!isDescendant && dragged.id !== node.id && dragged.parentId !== node.id) {
    emit('moveNode', dragged, node)
  }
}

const handleDragEnd = () => {
  hoveringNodeId.value = null
}

const handleContainerDragOver = (event: DragEvent) => {
  if (!props.draggable) {
    return
  }

  if (event.target !== event.currentTarget) {
    return
  }
  event.preventDefault()
  // 拖拽到根节点
  hoveringNodeId.value = props.rootId
}

const handleContainerDragLeave = (event: DragEvent) => {
  if (!props.draggable) {
    return
  }

  if (event.target !== event.currentTarget) {
    return
  }
  // drag时光标超出了drag zone,清除悬停框
  hoveringNodeId.value = null
}
</script>

<style lang="less" scoped>
.draggable-tree {
  padding: 6px 0;
  overflow-x: auto;
  .row {
    width: fit-content;
    min-width: 100%;
    height: 24px;
    padding: 0 12px;
    margin: 0 -1px;
    display: flex;
    align-items: center;
    &,
    * {
      cursor: pointer;
    }
    &:hover {
      background-color: var(--te-page-manage-draggable-row-bg-color-hover);
    }

    .content {
      flex: 1;
      display: flex;
      align-items: center;
      overflow: hidden;
    }
    label {
      flex: 1;
      font-size: 12px;
      line-height: 18px;
      white-space: nowrap;
    }
    .prefix-icon {
      display: flex;
      align-items: center;
      margin-right: 8px;
      svg {
        color: var(--te-page-manage-draggable-icon-color);
      }
    }
    &.active {
      background-color: var(--te-page-manage-draggable-row-bg-color-hover);
      color: var(--te-page-manage-draggable-text-color);
      svg {
        color: var(--te-page-manage-draggable-icon-color);
      }
    }
  }
}
.border-transparent {
  border: 1px solid transparent;
}
.hover-border-color {
  border-color: var(--te-page-manage-draggable-border-color);
  overflow: hidden;
}
</style>