<template>
  <div class="relation-graph-wrapper">
    <div class="graph-controls">
      <div v-for="item in controlBtns" :key="item.type"
        :class="['control-btn', { 'control-btn-active': currentLayoutType === item.type }]"
        @click="handleClickControlBtn(item.type)">
        <img :src="item.icon" alt="" />
      </div>
    </div>
    <div ref="containerRef" class="graph-container"></div>

    <div v-if="selectedNode" class="node-popup">
      <div class="popup-header">
        <img :src="selectedNode.image || defaultIcon" alt="" class="popup-icon" />
        <div class="popup-title">{{ selectedNode.name }}</div>
        <el-icon class="close-icon" @click="selectedNode = null">
          <Close />
        </el-icon>
      </div>
      <div class="popup-body">
        <div v-if="selectedNode.isSanctioned" class="tag-row">
          <span class="red-dot"></span>
          <span class="red-text">被制裁实体</span>
        </div>
        <p class="desc">{{ selectedNode.description || '暂无描述' }}</p>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
import G6 from '@antv/g6'
import { Close } from '@element-plus/icons-vue'
import echartsIcon01 from '../assets/echartsicon01.png'
import echartsIcon02 from '../assets/echartsicon02.png'
import echartsIcon03 from '../assets/echartsicon03.png'
import defaultIcon from '../assets/echartsicon03.png'

const props = defineProps({
  graphData: {
    type: Object,
    default: () => ({ nodes: [], links: [] })
  },
  treeData: {
    type: Object,
    default: () => null
  },
  controlActive: {
    type: Number,
    default: 1
  }
})

const emit = defineEmits(['nodeClick', 'layoutChange'])

const containerRef = ref(null)
const graphInstance = ref(null)
const currentLayoutType = ref(1)
const selectedNode = ref(null)

const controlBtns = [
  { type: 1, icon: echartsIcon01, name: '力导向布局' },
  { type: 2, icon: echartsIcon02, name: '树布局' },
  { type: 3, icon: echartsIcon03, name: '环状布局' }
]

const initGraph = (layoutType = 1) => {
  if (!containerRef.value) return

  destroyGraph()

  nextTick(() => {
    const container = containerRef.value
    const width = container.offsetWidth || 800
    const height = container.offsetHeight || 600

    if (layoutType === 2) {
      initTreeGraph(width, height)
    } else if (layoutType === 3) {
      initCircularGraph(width, height)
    } else {
      initNormalGraph(layoutType, width, height)
    }
  })
}

const initNormalGraph = (layoutType, width, height) => {
  const data = processGraphData(props.graphData)

  if (!data.nodes || data.nodes.length === 0) return

  const layout = {
    type: 'force',
    center: [width / 2, height / 2],
    preventOverlap: true,
    nodeSpacing: 80,
    linkDistance: 250,
    nodeStrength: -800,
    edgeStrength: 0.1,
    collideStrength: 0.8,
    alphaDecay: 0.01,
    alphaMin: 0.001
  }

  graphInstance.value = new G6.Graph({
    container: containerRef.value,
    width,
    height,
    fitView: true,
    fitViewPadding: 100,
    fitCenter: true,
    animate: true,
    animateCfg: {
      duration: 300,
      easing: 'easeLinear'
    },
    minZoom: 0.1,
    maxZoom: 10,
    modes: {
      default: [
        'drag-canvas',
        'zoom-canvas',
        'drag-node',
        {
          type: 'activate-relations',
          trigger: 'mouseenter',
          resetSelected: true
        }
      ]
    },
    layout,
    defaultNode: {
      type: 'image',
      size: 40,
      clipCfg: {
        show: true,
        type: 'circle',
        r: 20
      },
      labelCfg: {
        position: 'bottom',
        offset: 10,
        style: {
          fill: '#333',
          fontSize: 11,
          fontFamily: 'Microsoft YaHei',
          textAlign: 'center',
          background: {
            fill: 'rgba(255, 255, 255, 0.95)',
            padding: [4, 6, 4, 6],
            radius: 4
          }
        }
      }
    },
    defaultEdge: {
      type: 'quadratic',
      style: {
        stroke: '#5B8FF9',
        lineWidth: 3,
        opacity: 0.9,
        endArrow: {
          path: 'M 0,0 L 12,6 L 12,-6 Z',
          fill: '#5B8FF9'
        }
      },
      labelCfg: {
        autoRotate: true,
        style: {
          fill: '#333',
          fontSize: 10,
          fontFamily: 'Microsoft YaHei',
          background: {
            fill: '#fff',
            padding: [2, 4, 2, 4],
            radius: 2
          }
        }
      }
    },
    nodeStateStyles: {
      active: {
        shadowColor: '#1459BB',
        shadowBlur: 15,
        stroke: '#1459BB',
        lineWidth: 3
      },
      inactive: {
        opacity: 0.3
      }
    },
    edgeStateStyles: {
      active: {
        stroke: '#1459BB',
        lineWidth: 4
      },
      inactive: {
        opacity: 0.15
      }
    }
  })

  graphInstance.value.data(data)
  graphInstance.value.render()
  bindGraphEvents()
}

const initCircularGraph = (width, height) => {
  const data = processGraphData(props.graphData)

  if (!data.nodes || data.nodes.length === 0) return

  const centerX = width / 2
  const centerY = height / 2
  const radius = Math.min(width, height) / 2 - 120

  const otherNodes = data.nodes.filter(n => !n.isCenter)
  const nodeCount = otherNodes.length

  otherNodes.forEach((node, index) => {
    const angle = (2 * Math.PI * index) / nodeCount - Math.PI / 2
    node.x = centerX + radius * Math.cos(angle)
    node.y = centerY + radius * Math.sin(angle)
  })

  const centerNode = data.nodes.find(n => n.isCenter)
  if (centerNode) {
    centerNode.x = centerX
    centerNode.y = centerY
    centerNode.fx = centerX
    centerNode.fy = centerY
  }

  graphInstance.value = new G6.Graph({
    container: containerRef.value,
    width,
    height,
    fitView: false,
    fitCenter: false,
    animate: true,
    animateCfg: {
      duration: 300,
      easing: 'easeLinear'
    },
    minZoom: 0.1,
    maxZoom: 10,
    modes: {
      default: [
        'drag-canvas',
        'zoom-canvas',
        'drag-node',
        {
          type: 'activate-relations',
          trigger: 'mouseenter',
          resetSelected: true
        }
      ]
    },
    defaultNode: {
      type: 'image',
      size: 40,
      clipCfg: {
        show: true,
        type: 'circle',
        r: 20
      },
      labelCfg: {
        position: 'bottom',
        offset: 10,
        style: {
          fill: '#333',
          fontSize: 11,
          fontFamily: 'Microsoft YaHei',
          textAlign: 'center',
          background: {
            fill: 'rgba(255, 255, 255, 0.95)',
            padding: [4, 6, 4, 6],
            radius: 4
          }
        }
      }
    },
    defaultEdge: {
      type: 'quadratic',
      style: {
        stroke: '#5B8FF9',
        lineWidth: 3,
        opacity: 0.9,
        endArrow: {
          path: 'M 0,0 L 12,6 L 12,-6 Z',
          fill: '#5B8FF9'
        }
      },
      labelCfg: {
        autoRotate: true,
        style: {
          fill: '#333',
          fontSize: 10,
          fontFamily: 'Microsoft YaHei',
          background: {
            fill: '#fff',
            padding: [2, 4, 2, 4],
            radius: 2
          }
        }
      }
    },
    nodeStateStyles: {
      active: {
        shadowColor: '#1459BB',
        shadowBlur: 15,
        stroke: '#1459BB',
        lineWidth: 3
      },
      inactive: {
        opacity: 0.3
      }
    },
    edgeStateStyles: {
      active: {
        stroke: '#1459BB',
        lineWidth: 4
      },
      inactive: {
        opacity: 0.15
      }
    }
  })

  graphInstance.value.data(data)
  graphInstance.value.render()
  bindGraphEvents()
}

const initTreeGraph = (width, height) => {
  const treeDataSource = convertGraphToTree(props.graphData)

  if (!treeDataSource) return

  graphInstance.value = new G6.TreeGraph({
    container: containerRef.value,
    width,
    height,
    fitView: true,
    fitViewPadding: 80,
    animate: true,
    animateCfg: {
      duration: 300,
      easing: 'easeLinear'
    },
    minZoom: 0.1,
    maxZoom: 10,
    modes: {
      default: [
        'drag-canvas',
        'zoom-canvas',
        'drag-node',
        {
          type: 'collapse-expand',
          onChange: function onChange(item, collapsed) {
            const data = item.getModel()
            data.collapsed = collapsed
            return true
          }
        }
      ]
    },
    layout: {
      type: 'compactBox',
      direction: 'LR',
      getId: function getId(d) {
        return d.id
      },
      getHeight: function getHeight() {
        return 16
      },
      getWidth: function getWidth() {
        return 16
      },
      getVGap: function getVGap() {
        return 30
      },
      getHGap: function getHGap() {
        return 120
      }
    },
    defaultNode: {
      type: 'image',
      size: 40,
      clipCfg: {
        show: true,
        type: 'circle',
        r: 20
      },
      labelCfg: {
        position: 'right',
        offset: 10,
        style: {
          fill: '#333',
          fontSize: 11,
          fontFamily: 'Microsoft YaHei',
          background: {
            fill: 'rgba(255, 255, 255, 0.95)',
            padding: [4, 6, 4, 6],
            radius: 4
          }
        }
      }
    },
    defaultEdge: {
      type: 'cubic-horizontal',
      style: {
        stroke: '#5B8FF9',
        lineWidth: 3
      }
    },
    nodeStateStyles: {
      active: {
        shadowColor: '#1459BB',
        shadowBlur: 15,
        stroke: '#1459BB',
        lineWidth: 3
      }
    }
  })

  graphInstance.value.data(treeDataSource)
  graphInstance.value.render()
  graphInstance.value.fitView()
  bindGraphEvents()
}

const convertGraphToTree = (graphData) => {
  if (!graphData || !graphData.nodes || graphData.nodes.length === 0) {
    return null
  }

  const nodes = graphData.nodes
  const links = graphData.links || graphData.edges || []
  const centerNode = nodes[0]
  const centerId = String(centerNode.id || '0')

  const childIdSet = new Set()
  const childrenNodes = []

  links.forEach((link) => {
    const source = String(link.source)
    const target = String(link.target)

    if (source === centerId && !childIdSet.has(target)) {
      const node = nodes.find(n => String(n.id) === target)
      if (node) {
        childIdSet.add(target)
        childrenNodes.push({
          id: target,
          label: node.name || '',
          img: node.image || defaultIcon,
          size: node.symbolSize || 40,
          name: node.name,
          image: node.image,
          isSanctioned: node.isSanctioned
        })
      }
    } else if (target === centerId && !childIdSet.has(source)) {
      const node = nodes.find(n => String(n.id) === source)
      if (node) {
        childIdSet.add(source)
        childrenNodes.push({
          id: source,
          label: node.name || '',
          img: node.image || defaultIcon,
          size: node.symbolSize || 40,
          name: node.name,
          image: node.image,
          isSanctioned: node.isSanctioned
        })
      }
    }
  })

  if (childrenNodes.length === 0) {
    nodes.slice(1).forEach((node) => {
      const nodeId = String(node.id)
      if (!childIdSet.has(nodeId)) {
        childIdSet.add(nodeId)
        childrenNodes.push({
          id: nodeId,
          label: node.name || '',
          img: node.image || defaultIcon,
          size: node.symbolSize || 40,
          name: node.name,
          image: node.image,
          isSanctioned: node.isSanctioned
        })
      }
    })
  }

  return {
    id: centerId,
    label: centerNode.name || '',
    img: centerNode.image || defaultIcon,
    size: centerNode.symbolSize || 60,
    name: centerNode.name,
    image: centerNode.image,
    isSanctioned: centerNode.isSanctioned,
    children: childrenNodes
  }
}

const processGraphData = (rawData) => {
  if (!rawData || !rawData.nodes || rawData.nodes.length === 0) {
    return { nodes: [], edges: [] }
  }

  const nodeMap = new Map()
  const nodes = []

  rawData.nodes.forEach((node, index) => {
    const nodeId = String(node.id || index)

    if (nodeMap.has(nodeId)) {
      return
    }
    nodeMap.set(nodeId, true)

    const isCenter = index === 0
    const size = node.symbolSize || (isCenter ? 60 : 40)

    nodes.push({
      id: nodeId,
      label: node.name || '',
      img: node.image || defaultIcon,
      size,
      isCenter,
      clipCfg: {
        show: true,
        type: 'circle',
        r: size / 2
      },
      style: {
        cursor: 'pointer'
      },
      labelCfg: {
        position: 'bottom',
        offset: 12,
        style: {
          fill: isCenter ? '#1459BB' : '#333',
          fontSize: isCenter ? 13 : 11,
          fontWeight: isCenter ? 'bold' : 'normal',
          fontFamily: 'Microsoft YaHei',
          textAlign: 'center'
        }
      },
      ...node,
      id: nodeId
    })
  })

  const edgeMap = new Map()
  const edges = []

  const rawEdges = rawData.links || rawData.edges || []
  rawEdges.forEach((edge, index) => {
    const source = String(edge.source)
    const target = String(edge.target)
    const edgeKey = `${source}-${target}`

    if (edgeMap.has(edgeKey)) {
      return
    }
    if (!nodeMap.has(source) || !nodeMap.has(target)) {
      return
    }
    edgeMap.set(edgeKey, true)

    edges.push({
      id: `edge-${index}`,
      source,
      target,
      label: edge.name || ''
    })
  })

  return { nodes, edges }
}

const bindGraphEvents = () => {
  if (!graphInstance.value) return

  graphInstance.value.on('node:click', (evt) => {
    const node = evt.item
    const model = node.getModel()
    selectedNode.value = model
    emit('nodeClick', model)
  })

  graphInstance.value.on('canvas:click', () => {
    selectedNode.value = null
  })
}

const handleClickControlBtn = (btn) => {
  currentLayoutType.value = btn
  emit('layoutChange', btn)
  initGraph(btn)
}

const destroyGraph = () => {
  if (graphInstance.value) {
    graphInstance.value.destroy()
    graphInstance.value = null
  }
}

const handleResize = () => {
  if (graphInstance.value && containerRef.value) {
    const width = containerRef.value.offsetWidth
    const height = containerRef.value.offsetHeight
    graphInstance.value.changeSize(width, height)
    graphInstance.value.fitView()
  }
}

watch(
  () => props.graphData,
  () => {
    initGraph(currentLayoutType.value)
  },
  { deep: true }
)

watch(
  () => props.treeData,
  () => {
    if (currentLayoutType.value === 2) {
      initGraph(2)
    }
  },
  { deep: true }
)

watch(
  () => props.controlActive,
  (newVal) => {
    if (newVal !== currentLayoutType.value) {
      handleClickControlBtn(newVal)
    }
  }
)

onMounted(() => {
  initGraph(1)
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
  destroyGraph()
})

defineExpose({
  refresh: () => initGraph(currentLayoutType.value),
  changeLayout: (type) => handleClickControlBtn(type),
  getGraph: () => graphInstance.value
})
</script>

<style lang="scss" scoped>
.relation-graph-wrapper {
  position: relative;
  width: 100%;
  height: 100%;
}

.graph-container {
  width: 100%;
  height: 100%;
}

.graph-controls {
  position: absolute;
  top: 16px;
  right: 16px;
  display: flex;
  gap: 8px;
  z-index: 10;

  .control-btn {
    width: 32px;
    height: 32px;
    border-radius: 4px;
    border: 1px solid rgba(234, 236, 238, 1);
    background: rgba(255, 255, 255, 1);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;

    img {
      width: 16px;
      height: 16px;
    }

    &:hover {
      border-color: rgba(5, 95, 194, 0.5);
    }
  }

  .control-btn-active {
    border-color: rgba(5, 95, 194, 1);
    background: rgba(231, 243, 255, 1);
  }
}

.node-popup {
  position: absolute;
  bottom: 16px;
  left: 16px;
  width: 320px;
  background: rgba(255, 255, 255, 1);
  border-radius: 8px;
  box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.1);
  border: 1px solid rgba(234, 236, 238, 1);
  z-index: 20;

  .popup-header {
    display: flex;
    align-items: center;
    padding: 12px 16px;
    border-bottom: 1px solid rgba(234, 236, 238, 1);

    .popup-icon {
      width: 32px;
      height: 32px;
      margin-right: 8px;
      border-radius: 50%;
      object-fit: cover;
    }

    .popup-title {
      flex: 1;
      font-size: 16px;
      font-weight: 700;
      font-family: "Microsoft YaHei";
      color: rgba(59, 65, 75, 1);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }

    .close-icon {
      cursor: pointer;
      color: rgba(132, 136, 142, 1);
      font-size: 16px;

      &:hover {
        color: rgba(5, 95, 194, 1);
      }
    }
  }

  .popup-body {
    padding: 12px 16px;

    .tag-row {
      display: flex;
      align-items: center;
      margin-bottom: 8px;

      .red-dot {
        width: 6px;
        height: 6px;
        border-radius: 50%;
        background: rgba(245, 63, 63, 1);
        margin-right: 8px;
      }

      .red-text {
        font-size: 14px;
        font-family: "Microsoft YaHei";
        color: rgba(245, 63, 63, 1);
      }
    }

    .desc {
      font-size: 14px;
      font-family: "Microsoft YaHei";
      line-height: 22px;
      color: rgba(95, 101, 108, 1);
      margin: 0;
    }
  }
}
</style>