<template>
  <div class="w-full h-full flex flex-col gap-8px">
    <div class="p-8px">
      <el-input v-model="search" clearable :prefix-icon="Search" placeholder="搜索" />
    </div>

    <div class="flex-1 overflow-hidden" ref="treeBoxRef">
      <el-scrollbar class="h-full">
        <el-tree
          ref="hierarchyTreeRef"
          style="max-width: 600px"
          node-key="id"
          draggable
          :allow-drag="allowDrag"
          :allow-drop="allowDrop"
          :height="treeHeight"
          :default-expanded-keys="expandedNodeKeys"
          :highlight-current="false"
          :data="hierarchyTreeData"
          :props="{ label: 'name', children: 'children' }"
          :filter-node-method="filterNode"
          @node-click="handleNodeClick"
          @node-drop="handleNodeDrop"
          @node-expand="handleNodeExpand"
          @node-collapse="handleNodeCollapse"
        >
          <template #default="{ node, data }">
            <ScreenTreeItem
              v-if="isScreenOrPageNode(data)"
              :node="node"
              :data="getScreenOrPageRawNode(data)"
            />
            <PageTreeItem v-else :node="node" :data="getWidgetRawNode(data)" />
          </template>
        </el-tree>
      </el-scrollbar>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { Search } from '@element-plus/icons-vue'
import { useElementSize } from '@vueuse/core'

import ScreenTreeItem from './components/ScreenTreeItem.vue'
import PageTreeItem from './components/PageTreeItem.vue'
import { useProjectStore } from '@/store/modules/project'

import type { BaseWidget } from '@/types/baseWidget'
import type { Page } from '@/types/page'
import type { Screen } from '@/types/screen'
import type {
  AllowDragFunction,
  AllowDropFunction,
  FilterNodeMethodFunction,
  NodeDropType,
  TreeInstance,
  TreeNodeData
} from 'element-plus'

type HierarchyNodeData = {
  id: string
  name: string
  nodeType: 'screen' | 'page' | 'widget'
  raw: Screen | Page | BaseWidget
  screenId: string
  pageId?: string
  children: HierarchyNodeData[]
}

const projectStore = useProjectStore()
const search = ref('')
const hierarchyTreeRef = ref<TreeInstance | null>(null)
const treeBoxRef = ref<HTMLDivElement | null>(null)
const expandedNodeKeySet = ref(new Set<string>())
const hasInitializedExpandedKeys = ref(false)

const { height } = useElementSize(treeBoxRef, {
  height: 100,
  width: 100
})
const treeHeight = computed(() => Math.max(height.value || 0, 100))

const getHierarchyNode = (data: TreeNodeData): HierarchyNodeData => data as HierarchyNodeData
const getScreenOrPageRawNode = (data: TreeNodeData): Screen | Page =>
  getHierarchyNode(data).raw as Screen | Page
const getWidgetRawNode = (data: TreeNodeData): BaseWidget =>
  getHierarchyNode(data).raw as BaseWidget
const isScreenOrPageNode = (data: TreeNodeData) => {
  const node = getHierarchyNode(data)
  return node.nodeType === 'screen' || node.nodeType === 'page'
}

const buildWidgetNodes = (
  widgets: BaseWidget[] = [],
  screenId: string,
  pageId: string
): HierarchyNodeData[] => {
  return widgets.map((widget) => ({
    id: widget.id,
    name: widget.name,
    nodeType: 'widget',
    raw: widget,
    screenId,
    pageId,
    children: buildWidgetNodes(widget.children || [], screenId, pageId)
  }))
}

const hierarchyTreeData = computed<HierarchyNodeData[]>(() => {
  return (
    projectStore.project?.screens.map((screen) => ({
      id: screen.id,
      name: screen.name,
      nodeType: 'screen',
      raw: screen,
      screenId: screen.id,
      children: screen.pages.map((page) => ({
        id: page.id,
        name: page.name,
        nodeType: 'page',
        raw: page,
        screenId: screen.id,
        pageId: page.id,
        children: buildWidgetNodes(page.children || [], screen.id, page.id)
      }))
    })) || []
  )
})

const collectNodeKeys = (nodes: HierarchyNodeData[]) => {
  const keys: string[] = []

  const walk = (items: HierarchyNodeData[]) => {
    items.forEach((item) => {
      keys.push(item.id)
      walk(item.children || [])
    })
  }

  walk(nodes)

  return keys
}

const collectCurrentAndDescendantNodeKeys = (node: HierarchyNodeData) => {
  return collectNodeKeys([node])
}

const expandedNodeKeys = computed(() => [...expandedNodeKeySet.value])

watch(
  hierarchyTreeData,
  (nodes) => {
    const nodeKeys = collectNodeKeys(nodes)

    if (!hasInitializedExpandedKeys.value) {
      expandedNodeKeySet.value = new Set(nodeKeys)
      hasInitializedExpandedKeys.value = true
      return
    }

    const existingKeys = new Set(nodeKeys)
    expandedNodeKeySet.value = new Set(
      [...expandedNodeKeySet.value].filter((key) => existingKeys.has(key))
    )
  },
  {
    immediate: true
  }
)

const handleNodeExpand = (nodeData: HierarchyNodeData) => {
  expandedNodeKeySet.value = new Set(expandedNodeKeySet.value).add(nodeData.id)
}

const handleNodeCollapse = (nodeData: HierarchyNodeData) => {
  const next = new Set(expandedNodeKeySet.value)
  collectCurrentAndDescendantNodeKeys(nodeData).forEach((key) => next.delete(key))
  expandedNodeKeySet.value = next
}

const setOpenedPage = (screenId?: string, pageId?: string) => {
  if (!screenId || !pageId) return

  const screenIndex =
    projectStore.project?.screens.findIndex((screen) => screen.id === screenId) ?? -1
  if (screenIndex !== -1) {
    projectStore.openPageIds[screenIndex] = pageId
  }
}

const handleNodeClick = (nodeData: HierarchyNodeData, _node: unknown, e?: MouseEvent) => {
  if (nodeData.nodeType === 'page') {
    projectStore.activePageId = nodeData.id
    setOpenedPage(nodeData.screenId, nodeData.id)
    projectStore.setSelectWidgets([])
    return
  }

  if (nodeData.nodeType !== 'widget' || !e || !nodeData?.id) return

  const pageChanged = !!nodeData.pageId && nodeData.pageId !== projectStore.activePageId
  if (nodeData.pageId) {
    projectStore.activePageId = nodeData.pageId
    setOpenedPage(nodeData.screenId, nodeData.pageId)
  }

  if (e.ctrlKey && !pageChanged) {
    projectStore.activeWidgets.push(nodeData.raw as BaseWidget)
  } else {
    projectStore.setSelectWidgets([nodeData.raw as BaseWidget])
  }
}

const matchesKeyword = (keyword: string, node: HierarchyNodeData) => {
  const currentName = String(node.raw.name || '').toLowerCase()
  if (currentName.includes(keyword)) return true

  return node.children.some((child) => matchesKeyword(keyword, child))
}

const filterNode: FilterNodeMethodFunction = (value, data) => {
  const keyword = String(value || '')
    .trim()
    .toLowerCase()
  if (!keyword) return true

  return matchesKeyword(keyword, getHierarchyNode(data))
}

watch(search, (value) => {
  hierarchyTreeRef.value?.filter(value.trim())
})

const isDescendantNode = (node: HierarchyNodeData, childId: string) => {
  return node.children.some((item) => item.id === childId || isDescendantNode(item, childId))
}

const hasLockedWidgetTree = (node: HierarchyNodeData): boolean => {
  return (
    !!(node.raw as BaseWidget).locked || node.children.some((child) => hasLockedWidgetTree(child))
  )
}

const allowDrag: AllowDragFunction = (node) => {
  const currentNode = getHierarchyNode(node.data)
  return (
    currentNode.nodeType === 'widget' &&
    !hasLockedWidgetTree(currentNode) &&
    !!currentNode.pageId &&
    currentNode.pageId === projectStore.activePageId
  )
}

const allowDrop: AllowDropFunction = (dragNode, dropNode, type) => {
  const dragData = getHierarchyNode(dragNode.data)
  const dropData = getHierarchyNode(dropNode.data)

  if (dragData.nodeType !== 'widget' || dropData.nodeType === 'screen') return false
  if (hasLockedWidgetTree(dragData)) return false
  if (!dragData.pageId || !dropData.pageId || dragData.pageId !== dropData.pageId) return false
  if (dragData.id === dropData.id || isDescendantNode(dragData, dropData.id)) return false

  if (dropData.nodeType === 'page') {
    return type === 'inner'
  }

  if (type === 'inner') {
    return !!(dropData.raw as BaseWidget).children
  }

  return true
}

const findPageById = (pageId?: string) => {
  if (!pageId) return

  return projectStore.project?.screens
    .flatMap((screen) => screen.pages)
    .find((page) => page.id === pageId)
}

const findWidgetListById = (widgets: BaseWidget[], widgetId: string) => {
  for (const widget of widgets) {
    if (widget.id === widgetId) {
      return widgets
    }

    if (widget.children?.length) {
      const found = findWidgetListById(widget.children, widgetId)
      if (found) {
        return found
      }
    }
  }
}

const takeWidgetFromPage = (page: Page, widgetId: string) => {
  const list = findWidgetListById(page.children || [], widgetId)
  if (!list) return

  const index = list.findIndex((widget) => widget.id === widgetId)
  if (index === -1) return

  return list.splice(index, 1)[0]
}

type TreeDragNode = {
  data: TreeNodeData
}

const handleNodeDrop = (dragNode: TreeDragNode, dropNode: TreeDragNode, dropType: NodeDropType) => {
  const dragData = getHierarchyNode(dragNode.data)
  const targetData = getHierarchyNode(dropNode.data)

  if (dropType === 'none') return

  if (dragData.nodeType !== 'widget' || !dragData.pageId || dragData.pageId !== targetData.pageId) {
    return
  }
  if (hasLockedWidgetTree(dragData)) return

  const page = findPageById(dragData.pageId)
  if (!page) return

  const widget = takeWidgetFromPage(page, dragData.id)
  if (!widget) return

  if (dropType === 'inner') {
    const target = targetData.raw as Page | BaseWidget
    target.children = target.children || []
    target.children.push(widget)
  } else {
    const targetList = findWidgetListById(page.children || [], targetData.id)
    if (!targetList) return

    const index = targetList.findIndex((item) => item.id === targetData.id)
    if (index === -1) return

    targetList.splice(dropType === 'before' ? index : index + 1, 0, widget)
  }

  projectStore.setSelectWidgets([widget])
}
</script>
