/**
 * @typedef {{Map.<nodeId: number, adjacentNodeIds: AdjacentNodes>}} AdjacencyList
 * @typedef {{Map.<nodeId: number, link: { distance: number }}>} AdjacentNodes
 */

/**
 * Create adjacency list from graph
 * @param {Object[]} nodes [{id: Number}, ...]
 * @param {Object[]} links [{id: Number, source: id, target: id, [direction]: boolean, [distance]: number ...}, ...]
 *
 * @returns {AdjacencyList} adjacencyList, where key is node id, and value a Set of adjacent node ids
 */
export function createAdjacencyList(nodes, links) {
  const adjacencyList = new Map()

  for (const node of nodes) {
    const adjacentNodes = new Map()

    for (const link of links) {
      const source = link.source
      const target = link.target

      if (source === node.uuid) adjacentNodes.set(target, { distance: 1 })
      else if (target === node.uuid) adjacentNodes.set(source, { distance: 1 })
    }

    adjacencyList.set(node.uuid, adjacentNodes)
  }

  return adjacencyList
}

/**
 * Dijkstra SSSP from a specified node
 * @param {AdjacencyList} adjacencyList graph
 * @param {number} startNode id
 */
export function runDijkstra(adjacencyList, startNode) {
  const visited = new Set()
  const predecessors = new Map()
  const distanceFromS = new Map()
  distanceFromS.set(startNode, 0)

  let currentNode = startNode
  while (currentNode !== null) {
    const currentDist = distanceFromS.get(currentNode)
    const adjacentNodes = adjacencyList.get(currentNode)

    for (const [neighbour, link] of adjacentNodes) {
      if (visited.has(neighbour)) continue

      const newDist = currentDist + link.distance
      const neighbourDist = distanceFromS.get(neighbour)
      // relax
      if (neighbourDist === undefined || neighbourDist > newDist) {
        distanceFromS.set(neighbour, newDist)
        predecessors.set(neighbour, currentNode)
      }
    }

    visited.add(currentNode)
    currentNode = chooseClosestNode(distanceFromS, visited)
  }

  return { distanceFromS, predecessors }
}

/**
 * @param {Map} distanceFromS
 * @param {Set} visited
 * @return {number} closest node id
 */
function chooseClosestNode(distanceFromS, visited) {
  let min = Infinity
  let closestNode = null
  for (const [node, dist] of distanceFromS) {
    if (visited.has(node)) continue

    if (dist < min) {
      min = dist
      closestNode = node
    }
  }
  return closestNode
}
