<script setup>
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
import cytoscape from 'cytoscape'
import coseBilkent from 'cytoscape-cose-bilkent'

// 注册 layout extension（兼容 ESM/UMD 多种导出形式）
const layoutExt = coseBilkent.default || coseBilkent
cytoscape.use(layoutExt)

const props = defineProps({
  // { nodes: [...], edges: [...], truncated, durationMs }
  graphData: { type: Object, default: null },
  loading: { type: Boolean, default: false }
})
const emit = defineEmits(['select'])

const container = ref(null)
let cy = null

function buildNodeData(n) {
  return {
    id: n.id,
    labels: n.labels,
    label: n.label,
    properties: n.properties
  }
}

function buildEdgeData(e) {
  return {
    id: e.id,
    source: e.source,
    target: e.target,
    label: e.label,
    type: e.type,
    properties: e.properties
  }
}

function buildElements(data) {
  if (!data) return []
  const nodes = (data.nodes || []).map(n => ({ data: buildNodeData(n), group: 'nodes' }))
  const edges = (data.edges || []).map(e => ({ data: buildEdgeData(e), group: 'edges' }))
  return [...nodes, ...edges]
}

/**
 * 拖拽释放后，对距离过近的节点施加斥力，使整体动态平衡。
 * 被拖拽的节点保持原位，仅推开与其重叠/过近的节点。
 */
function relaxAfterDrag(draggedNode, options = {}) {
  if (!cy) return
  const minDistance = options.minDistance || 90
  const strength = options.strength || 0.35
  const iterations = options.iterations || 25
  const damping = options.damping || 0.85

  const nodes = cy.nodes()
  const positions = {}
  nodes.forEach(n => {
    const p = n.position()
    positions[n.id()] = { x: p.x, y: p.y }
  })

  for (let i = 0; i < iterations; i++) {
    const forces = {}
    nodes.forEach(n => {
      forces[n.id()] = { x: 0, y: 0 }
    })

    // 计算所有节点对之间的斥力
    for (let a = 0; a < nodes.length; a++) {
      for (let b = a + 1; b < nodes.length; b++) {
        const nodeA = nodes[a]
        const nodeB = nodes[b]
        const posA = positions[nodeA.id()]
        const posB = positions[nodeB.id()]
        const dx = posA.x - posB.x
        const dy = posA.y - posB.y
        const dist = Math.sqrt(dx * dx + dy * dy)

        if (dist < minDistance && dist > 0.001) {
          const force = (minDistance - dist) * strength
          const fx = (dx / dist) * force
          const fy = (dy / dist) * force
          forces[nodeA.id()].x += fx
          forces[nodeA.id()].y += fy
          forces[nodeB.id()].x -= fx
          forces[nodeB.id()].y -= fy
        }
      }
    }

    // 应用斥力（被拖拽节点保持原位）
    nodes.forEach(n => {
      if (n.id() === draggedNode.id()) return
      const f = forces[n.id()]
      positions[n.id()].x += f.x * damping
      positions[n.id()].y += f.y * damping
    })
  }

  // 使用动画移动到最终平衡位置
  nodes.forEach(n => {
    if (n.id() === draggedNode.id()) return
    n.animate({
      position: positions[n.id()],
      duration: 350,
      easing: 'ease-out'
    })
  })
}

function getLayoutOptions(layoutName, nodeCount) {
  if (layoutName === 'circle' || nodeCount > 500) {
    return {
      name: 'circle',
      padding: 30,
      fit: true,
      animate: true,
      animationDuration: 500,
      avoidOverlap: true,
      nodeSeparation: 40
    }
  }
  // 动态距离：让小图自然展开
  return {
    name: 'cose-bilkent',
    animate: 'end',
    animationDuration: 800,
    idealEdgeLength: 140,
    nodeRepulsion: 14000,
    edgeElasticity: 0.45,
    gravity: 0.25,
    gravityRange: 3.8,
    padding: 30,
    fit: true,
    randomize: true,
    nodeDimensionsIncludeLabels: true,
    // 节点数较少时关闭 tiling，避免强制网格排列抵消斥力；大图仍开启以保证性能
    tiling: nodeCount > 100,
    tilingPaddingVertical: 50,
    tilingPaddingHorizontal: 50,
    numIter: 8000
  }
}

function initCyto() {
  if (!container.value) return
  cy = cytoscape({
    container: container.value,
    elements: [],
    style: [
      {
        selector: 'node',
        style: {
          'label': 'data(label)',
          'background-color': '#2563eb',
          'color': '#e5e7eb',
          'font-size': '10px',
          'text-valign': 'bottom',
          'text-halign': 'center',
          'text-margin-y': 4,
          'text-outline-width': 2,
          'text-outline-color': '#1e1e1e',
          'width': 24,
          'height': 24,
          'border-width': 1,
          'border-color': '#1e40af'
        }
      },
      {
        selector: 'node:selected',
        style: {
          'background-color': '#f59e0b',
          'border-color': '#fbbf24',
          'border-width': 2
        }
      },
      {
        selector: 'edge',
        style: {
          'label': 'data(label)',
          'curve-style': 'bezier',
          'target-arrow-shape': 'triangle',
          'target-arrow-color': '#64748b',
          'line-color': '#475569',
          'width': 1.5,
          'color': '#94a3b8',
          'font-size': '9px',
          'text-background-color': '#1e1e1e',
          'text-background-padding': '2px',
          'text-background-opacity': 0.8,
          'text-background-shape': 'round-rectangle'
        }
      },
      {
        selector: 'edge:selected',
        style: {
          'line-color': '#f59e0b',
          'target-arrow-color': '#f59e0b',
          'width': 2.5
        }
      }
    ],
    layout: { name: 'grid' },
    wheelSensitivity: 0.2
  })

  cy.on('tap', 'node', (evt) => {
    emit('select', { type: 'node', data: evt.target.data() })
  })
  cy.on('tap', 'edge', (evt) => {
    emit('select', { type: 'edge', data: evt.target.data() })
  })
  cy.on('tap', (evt) => {
    if (evt.target === cy) {
      // 点击空白区域取消选择
      emit('select', null)
    }
  })

  // 拖拽释放后触发局部斥力平衡
  cy.on('dragfree', 'node', (evt) => {
    relaxAfterDrag(evt.target)
  })
}

function applyData(data) {
  if (!cy) return
  cy.elements().remove()
  const elements = buildElements(data)
  if (!elements.length) return
  cy.add(elements)
  // 500 节点内使用 cose-bilkent（高质量力导向布局），超过则用 circle（更轻量避免卡顿）
  const nodeCount = elements.filter(e => e.group === 'nodes').length
  cy.layout(getLayoutOptions(nodeCount <= 500 ? 'cose-bilkent' : 'circle', nodeCount)).run()
  cy.fit(undefined, 30)
}

watch(() => props.graphData, async (data) => {
  await nextTick()
  applyData(data)
})

onMounted(async () => {
  await nextTick()
  initCyto()
  if (props.graphData) {
    applyData(props.graphData)
  }
})

onBeforeUnmount(() => {
  if (cy) {
    cy.destroy()
    cy = null
  }
})

defineExpose({
  fitView: () => cy?.fit(undefined, 30),
  resetZoom: () => cy?.reset(),
  relayout: () => {
    if (!cy) return
    const nodeCount = cy.nodes().length
    cy.layout(getLayoutOptions('cose-bilkent', nodeCount)).run()
  }
})
</script>

<template>
  <div class="graph-viewer">
    <div ref="container" class="cy-container" />
    <div v-if="loading" class="loading-overlay">渲染中…</div>
    <div v-else-if="!graphData?.nodes?.length" class="empty-overlay">
      未加载数据。请输入 Cypher 后点击「查询」。
    </div>
  </div>
</template>

<style scoped>
.graph-viewer {
  position: relative;
  width: 100%;
  height: 100%;
  background: #1a1a1a;
  border-radius: 6px;
  overflow: hidden;
}

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

.loading-overlay,
.empty-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  font-size: 12px;
  background: rgba(26, 26, 26, 0.6);
  pointer-events: none;
}
</style>
