// 3D Force-directed layout — Web Worker
// Receives node count + edges, computes positions, sends Float32Array via Transferable

interface ForceParams {
  repulsion: number
  attraction: number
  gravity: number
  damping: number
  maxIterations: number
  restLength: number
}

interface InitMessage {
  type: 'init'
  nodeCount: number
  edges: [number, number][]
  params: ForceParams
}

type InMessage = InitMessage | { type: 'stop' }

let running = false
let positions: Float32Array
let velocities: Float32Array
let nodeCount = 0
let edges: [number, number][] = []
let params: ForceParams = {
  repulsion: 50, attraction: 0.006, gravity: 0.02,
  damping: 0.92, maxIterations: 300, restLength: 8,
}

function init(msg: InitMessage) {
  nodeCount = msg.nodeCount
  edges = msg.edges
  params = msg.params
  positions = new Float32Array(nodeCount * 3)
  velocities = new Float32Array(nodeCount * 3)

  // Random initial positions on a sphere surface
  for (let i = 0; i < nodeCount; i++) {
    const theta = Math.random() * Math.PI * 2
    const phi = Math.acos(2 * Math.random() - 1)
    const r = 20 * Math.cbrt(Math.random())
    positions[i * 3]     = r * Math.sin(phi) * Math.cos(theta)
    positions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta)
    positions[i * 3 + 2] = r * Math.cos(phi)
  }

  running = true
  simulate()
}

function simulate() {
  for (let iter = 0; iter < params.maxIterations && running; iter++) {
    tick()
    // Send positions every 5 iterations for smooth animation
    if (iter % 5 === 0 || iter === params.maxIterations - 1) {
      const copy = new Float32Array(positions)
      const settled = iter === params.maxIterations - 1
      ;(self.postMessage as Function)(
        { type: 'positions', positions: copy, settled, iteration: iter },
        [copy.buffer],
      )
    }
  }
  running = false
}

function tick() {
  const { repulsion, attraction, gravity, damping, restLength } = params
  const n = nodeCount

  const fx = new Float32Array(n)
  const fy = new Float32Array(n)
  const fz = new Float32Array(n)

  // Repulsion: O(n²) — fine for ~1300 nodes in a worker
  for (let i = 0; i < n; i++) {
    const ix = positions[i * 3], iy = positions[i * 3 + 1], iz = positions[i * 3 + 2]
    for (let j = i + 1; j < n; j++) {
      let dx = positions[j * 3] - ix
      let dy = positions[j * 3 + 1] - iy
      let dz = positions[j * 3 + 2] - iz
      let dist2 = dx * dx + dy * dy + dz * dz
      if (dist2 < 0.01) dist2 = 0.01
      const f = repulsion / dist2
      const dist = Math.sqrt(dist2)
      dx /= dist; dy /= dist; dz /= dist
      fx[i] -= f * dx; fy[i] -= f * dy; fz[i] -= f * dz
      fx[j] += f * dx; fy[j] += f * dy; fz[j] += f * dz
    }
  }

  // Attraction along edges
  for (const [si, ti] of edges) {
    const dx = positions[ti * 3] - positions[si * 3]
    const dy = positions[ti * 3 + 1] - positions[si * 3 + 1]
    const dz = positions[ti * 3 + 2] - positions[si * 3 + 2]
    const dist = Math.sqrt(dx * dx + dy * dy + dz * dz) || 0.01
    const f = attraction * (dist - restLength)
    const ux = dx / dist, uy = dy / dist, uz = dz / dist
    fx[si] += f * ux; fy[si] += f * uy; fz[si] += f * uz
    fx[ti] -= f * ux; fy[ti] -= f * uy; fz[ti] -= f * uz
  }

  // Central gravity + apply forces
  for (let i = 0; i < n; i++) {
    fx[i] -= gravity * positions[i * 3]
    fy[i] -= gravity * positions[i * 3 + 1]
    fz[i] -= gravity * positions[i * 3 + 2]

    velocities[i * 3]     = (velocities[i * 3] + fx[i]) * damping
    velocities[i * 3 + 1] = (velocities[i * 3 + 1] + fy[i]) * damping
    velocities[i * 3 + 2] = (velocities[i * 3 + 2] + fz[i]) * damping

    // Clamp velocity
    const speed = Math.sqrt(
      velocities[i * 3] ** 2 + velocities[i * 3 + 1] ** 2 + velocities[i * 3 + 2] ** 2,
    )
    if (speed > 2) {
      const scale = 2 / speed
      velocities[i * 3] *= scale
      velocities[i * 3 + 1] *= scale
      velocities[i * 3 + 2] *= scale
    }

    positions[i * 3]     += velocities[i * 3]
    positions[i * 3 + 1] += velocities[i * 3 + 1]
    positions[i * 3 + 2] += velocities[i * 3 + 2]
  }
}

self.onmessage = (e: MessageEvent<InMessage>) => {
  if (e.data.type === 'init') {
    init(e.data)
  } else if (e.data.type === 'stop') {
    running = false
  }
}
