<template>
  <el-tooltip content="回到根节点">
    <el-button link class="absolute left-10px top-10px" @click="handleToRoot"
      ><LuCrosshair size="14px"
    /></el-button>
  </el-tooltip>

  <div ref="containerRef" class="w-full h-full"></div>
</template>

<script setup lang="tsx">
import type { AppContext } from 'vue'
import type { NodeItemType, OptionType } from './type'
import type { WidgetEventTargetData } from '@/types/event'

import { createApp, defineComponent, getCurrentInstance, nextTick, ref, watch } from 'vue'
import MindMap from 'simple-mind-map'
import { bfsWalk } from 'simple-mind-map/src/utils'
import { klona } from 'klona'
import { v4 } from 'uuid'
import { useElementSize, useResizeObserver, watchOnce } from '@vueuse/core'
import { LuCrosshair } from 'vue-icons-plus/lu'

import defaultTheme from './theme'
import NodeItem from './NodeItem.vue'
import { useAppStore } from '@/store/modules/app'
import { useProjectStore } from '@/store/modules/project'
import { getActionOptions, getBuiltinActionData, isBuiltinTarget } from './config'

const containerRef = ref<HTMLElement | null>(null)
const { width, height } = useElementSize(containerRef)
const mindMap = ref<MindMap>()
const mindMapData = ref<NodeItemType>()

const projectStore = useProjectStore()
const appStore = useAppStore()
const appContext = getCurrentInstance()?.appContext

useResizeObserver(containerRef, () => {
  if (mindMap.value && width.value && height.value) {
    mindMap.value.resize()
  }
})

const getAddNode = (parentId: string, parentType: 'root' | 'event' | 'target'): NodeItemType => ({
  id: v4(),
  name: 'add_btn',
  nodeType: 'add',
  parentId,
  parentType
})

const handleToRoot = () => {
  mindMap.value?.view.reset()
}

const resetMindMapView = async () => {
  if (!mindMap.value) return

  await nextTick()
  mindMap.value.view.reset()
}

const createBuiltinActionNode = (
  targetId: string,
  targetData: WidgetEventTargetData,
  option?: OptionType
): NodeItemType | null => {
  if (targetData.kind === 'custom_code') {
    return {
      id: v4(),
      name: '自定义代码',
      nodeType: 'action',
      parentId: targetId,
      parentType: 'target',
      value: '',
      config: {
        data: {
          targetData,
          kind: 'custom_code'
        }
      }
    }
  }

  if (!option) return null

  return {
    id: v4(),
    name: option.label,
    nodeType: 'action',
    parentId: targetId,
    parentType: 'target',
    value: klona(option.defaultValue),
    config: {
      data: {
        targetData,
        actionData: option.data,
        kind: targetData.kind
      }
    }
  }
}

const createTargetChildren = (
  targetId: string,
  targetData: WidgetEventTargetData
): NodeItemType[] => {
  if (!isBuiltinTarget(targetData)) {
    return [getAddNode(targetId, 'target')]
  }

  const builtinActionData = getBuiltinActionData(targetData)
  const actionOptions = getActionOptions(
    targetData,
    projectStore.project,
    projectStore.activePage?.id
  )
  const actionOption = builtinActionData
    ? actionOptions.find((item) => item.value === builtinActionData.actionKey)
    : undefined
  const actionNode = createBuiltinActionNode(targetId, targetData, actionOption)

  return actionNode ? [actionNode] : []
}

const createTargetNode = (option: OptionType): NodeItemType => {
  const id = v4()
  const targetData = option.data as WidgetEventTargetData

  return {
    id,
    name: option.label,
    value: option.value,
    nodeType: 'target',
    config: {
      data: targetData
    },
    children: createTargetChildren(id, targetData)
  }
}

const findNodeById = (id: string) => {
  let result: NodeItemType | undefined

  bfsWalk(mindMapData.value, (node) => {
    if ((node as NodeItemType).id === id) {
      result = node as NodeItemType
    }
  })

  return result
}

const createActionNode = (parentId: string, option: OptionType): NodeItemType => {
  const parentNode = findNodeById(parentId)
  const targetData = parentNode?.config?.data as WidgetEventTargetData | undefined

  return {
    id: v4(),
    name: option.label,
    nodeType: 'action',
    parentId,
    parentType: 'target',
    value: klona(option.defaultValue),
    config: {
      data: {
        targetData,
        actionData: option.data,
        kind: targetData?.kind
      }
    }
  }
}

const shouldHaveAddNode = (node: NodeItemType) => {
  if (node.nodeType === 'root' || node.nodeType === 'event') return true
  if (node.nodeType !== 'target') return false

  const targetData = node.config?.data as WidgetEventTargetData | undefined
  return !isBuiltinTarget(targetData)
}

const attachUiNodes = (node: NodeItemType): NodeItemType => {
  if (node.nodeType === 'add' || node.nodeType === 'action') {
    return { ...node }
  }

  const children = (node.children || [])
    .filter((child) => child.nodeType !== 'add')
    .map((child) =>
      attachUiNodes({
        ...child,
        parentId: node.id,
        parentType: node.nodeType === 'root' ? 'root' : node.nodeType
      })
    )

  if (shouldHaveAddNode(node)) {
    children.push(getAddNode(node.id, node.nodeType === 'root' ? 'root' : node.nodeType))
  }

  return {
    ...node,
    children
  }
}

const sanitizeEventValue = (value: any, seen = new WeakSet<object>()) => {
  if (value === null || value === undefined) return value
  if (typeof value !== 'object') return value

  if (seen.has(value as object)) {
    return undefined
  }

  seen.add(value as object)

  if (Array.isArray(value)) {
    return value.map((item) => sanitizeEventValue(item, seen)).filter((item) => item !== undefined)
  }

  const result: Record<string, any> = {}

  Object.entries(value as Record<string, any>).forEach(([key, item]) => {
    if (key === '_node') return
    if (typeof item === 'function') return

    const nextValue = sanitizeEventValue(item, seen)
    if (nextValue !== undefined) {
      result[key] = nextValue
    }
  })

  return result
}

const stripUiNodes = (node?: NodeItemType): NodeItemType | undefined => {
  if (!node || node.nodeType === 'add') return

  const baseNode: NodeItemType = sanitizeEventValue({
    id: node.id,
    name: node.name,
    value: sanitizeEventValue(node.value),
    nodeType: node.nodeType,
    config: sanitizeEventValue(node.config)
  })

  if (node.children) {
    baseNode.children = node.children
      .map((child) => stripUiNodes(child))
      .filter(Boolean) as NodeItemType[]
  }

  delete baseNode.parentId
  delete baseNode.parentType
  return baseNode
}

const updateStoreEvents = (data?: NodeItemType) => {
  const widget = projectStore.activeWidget
  const sourceData = data || stripUiNodes(mindMapData.value)

  if (!widget || !sourceData || widget.id !== sourceData.id) return
  widget.events = (sourceData.children || []) as any
}

const refresh = () => {
  if (!mindMapData.value) return

  const cleanData = stripUiNodes(mindMapData.value)
  if (!cleanData) return

  const nextData = attachUiNodes(cleanData)
  mindMapData.value = nextData
  updateStoreEvents(cleanData)
  mindMap.value?.setData(nextData)
}

const getSiblingUsedValues = (parentId: string, currentNodeId?: string) => {
  const parentNode = findNodeById(parentId)
  if (!parentNode?.children) return new Set<string>()

  return new Set(
    parentNode.children
      .filter((item) => item.id !== currentNodeId && item.nodeType !== 'add')
      .map((item) => item.value)
      .filter(Boolean) as string[]
  )
}

const handleAddNode = (parentId: string, parentType: string, values: OptionType[]) => {
  const parentNode = findNodeById(parentId)
  if (!parentNode?.children?.length) return

  const usedValues = getSiblingUsedValues(parentId)
  const insertNodes: NodeItemType[] = []

  switch (parentType) {
    case 'root':
      values
        .filter((item) => !usedValues.has(item.value))
        .forEach((item) => {
          const id = v4()
          insertNodes.push({
            id,
            name: item.label,
            value: item.value,
            nodeType: 'event',
            config: {
              data: item.data
            },
            children: [getAddNode(id, 'event')]
          })
        })
      break

    case 'event':
      values
        .filter((item) => !usedValues.has(item.value))
        .forEach((item) => {
          insertNodes.push(createTargetNode(item))
        })
      break

    case 'target':
      values
        .filter((item) => !usedValues.has(item.value))
        .forEach((item) => {
          insertNodes.push(createActionNode(parentId, item))
        })
      break
  }

  if (!insertNodes.length) return

  parentNode.children.splice(parentNode.children.length - 1, 0, ...insertNodes)
  refresh()
}

const removeNodeById = (id: string) => {
  bfsWalk(mindMapData.value, (node) => {
    const currentNode = node as NodeItemType
    if (!currentNode.children?.length) return
    currentNode.children = currentNode.children.filter((item) => item.id !== id)
  })
}

const handleDeleteNode = (id: string) => {
  removeNodeById(id)
  refresh()
}

const handleChangeNode = (node: NodeItemType) => {
  if (!mindMapData.value) return

  bfsWalk(mindMapData.value, (item) => {
    const currentNode = item as NodeItemType
    if (currentNode.id !== node.id) return
    Object.assign(currentNode, node)
  })

  if (node.nodeType === 'action') {
    updateStoreEvents()
    return
  }

  refresh()
}

const buildRootNode = async () => {
  const widget = projectStore.activeWidget
  if (!widget) {
    mindMapData.value = undefined
    return
  }

  const rootNode = attachUiNodes({
    id: widget.id,
    name: widget.name,
    nodeType: 'root',
    config: null,
    children: klona(widget.events || []) as NodeItemType[]
  })

  mindMapData.value = rootNode
  await nextTick()
  mindMap.value?.setData(rootNode)
  await resetMindMapView()
}

const inheritParentAppContext = (childApp: ReturnType<typeof createApp>, parent?: AppContext) => {
  if (!parent) return

  Object.assign(childApp._context.components, parent.components)
  Object.assign(childApp._context.directives, parent.directives)
  Object.assign(childApp._context.config.globalProperties, parent.config.globalProperties)
  childApp._context.provides = parent.provides
}

watch(
  () => [projectStore.activeWidget?.id, appStore.compositeTabAcitve],
  async () => {
    await buildRootNode()
  },
  {
    immediate: true
  }
)

watch(
  () => mindMapData.value,
  () => {
    updateStoreEvents()
  },
  {
    deep: true
  }
)

watchOnce(
  () => [width.value, height.value],
  ([w, h]) => {
    if (!w || !h) return

    mindMap.value = new MindMap({
      el: containerRef.value,
      themeConfig: defaultTheme,
      initRootNodePosition: ['5%', 'center'],
      fitPadding: 12,
      data: mindMapData.value,
      readonly: true,
      isUseCustomNodeContent: true,
      customCreateNodeContent: (node: any) => {
        const sourceData = node.nodeData as NodeItemType

        const App = defineComponent({
          render() {
            return (
              <NodeItem
                node={sourceData}
                mindMapNode={node}
                onAdd={handleAddNode}
                onDelete={handleDeleteNode}
                onChange={handleChangeNode}
                mindMap={mindMap.value}
              />
            )
          }
        })

        const div = document.createElement('div')
        const app = createApp(App)
        inheritParentAppContext(app, appContext)
        div.style.userSelect = 'none'
        app.mount(div)
        return div
      }
    } as any)
  }
)
</script>
