///|
pub(all) enum GraphNode {
  Root
  Package(String, Version)
  Unresolved(String)
} derive(Eq)

///|
pub(all) struct DependencyEdge {
  from : GraphNode
  to : GraphNode
  requirement : String
} derive(Eq)

///|
pub(all) struct DependencyGraph {
  nodes : Array[GraphNode]
  edges : Array[DependencyEdge]
} derive(Eq)

///|
pub fn build_dependency_graph(
  root : Array[Dependency],
  resolution : Resolution,
) -> DependencyGraph {
  let packages = resolution.packages.copy()
  packages.sort_by(compare_package)
  let unresolved : Array[String] = []
  let edges : Array[DependencyEdge] = []
  for dependency in root {
    let target = dependency_target(packages, dependency.name, unresolved)
    edges.push({ from: Root, to: target, requirement: dependency.req.raw })
  }
  for item in packages {
    for dependency in item.dependencies {
      let target = dependency_target(packages, dependency.name, unresolved)
      edges.push({
        from: Package(item.name, item.version),
        to: target,
        requirement: dependency.req.raw,
      })
    }
  }
  unresolved.sort_by(compare_text)
  edges.sort_by(compare_edge)
  let nodes : Array[GraphNode] = [Root]
  for item in packages {
    nodes.push(Package(item.name, item.version))
  }
  for name in unresolved {
    nodes.push(Unresolved(name))
  }
  { nodes, edges }
}

///|
pub fn format_graph_text(graph : DependencyGraph) -> String {
  let builder = StringBuilder()
  for edge in graph.edges {
    builder.write_string(graph_node_label(edge.from))
    builder.write_string(" -> ")
    builder.write_string(graph_node_label(edge.to))
    builder.write_string(" [")
    builder.write_string(edge.requirement)
    builder.write_string("]\n")
  }
  builder.to_string()
}

///|
pub fn format_graph_dot(graph : DependencyGraph) -> String {
  let builder = StringBuilder()
  builder.write_string("digraph dependencies {\n")
  for index, node in graph.nodes {
    builder.write_string("  n\{index} [label=\"")
    builder.write_string(dot_escape(graph_node_label(node)))
    builder.write_string("\"];\n")
  }
  for edge in graph.edges {
    let from_index = graph_node_index(graph.nodes, edge.from)
    let to_index = graph_node_index(graph.nodes, edge.to)
    builder.write_string("  n\{from_index} -> n\{to_index} [label=\"")
    builder.write_string(dot_escape(edge.requirement))
    builder.write_string("\"];\n")
  }
  builder.write_string("}\n")
  builder.to_string()
}

///|
fn dependency_target(
  packages : Array[PackageVersion],
  name : String,
  unresolved : Array[String],
) -> GraphNode {
  for item in packages {
    if item.name == name {
      return Package(item.name, item.version)
    }
  }
  if !contains_text(unresolved, name) {
    unresolved.push(name)
  }
  Unresolved(name)
}

///|
fn contains_text(values : Array[String], expected : String) -> Bool {
  for value in values {
    if value == expected {
      return true
    }
  }
  false
}

///|
fn compare_package(left : PackageVersion, right : PackageVersion) -> Int {
  let name_order = compare_text(left.name, right.name)
  if name_order != 0 {
    name_order
  } else {
    compare_version(left.version, right.version)
  }
}

///|
fn compare_edge(left : DependencyEdge, right : DependencyEdge) -> Int {
  let from_order = compare_text(
    graph_node_sort_key(left.from),
    graph_node_sort_key(right.from),
  )
  if from_order != 0 {
    return from_order
  }
  let to_order = compare_text(
    graph_node_sort_key(left.to),
    graph_node_sort_key(right.to),
  )
  if to_order != 0 {
    return to_order
  }
  compare_text(left.requirement, right.requirement)
}

///|
fn graph_node_sort_key(node : GraphNode) -> String {
  match node {
    Root => "0:root"
    Package(name, version) => "1:\{name}@\{format_version(version)}"
    Unresolved(name) => "2:\{name}"
  }
}

///|
fn graph_node_label(node : GraphNode) -> String {
  match node {
    Root => "root"
    Package(name, version) => "\{name}@\{format_version(version)}"
    Unresolved(name) => "unresolved:\{name}"
  }
}

///|
fn graph_node_index(nodes : Array[GraphNode], target : GraphNode) -> Int {
  for index, node in nodes {
    if node == target {
      return index
    }
  }
  abort("dependency graph edge references an unknown node")
}

///|
fn dot_escape(input : String) -> String {
  input
  .replace_all(old="\\", new="\\\\")
  .replace_all(old="\"", new="\\\"")
  .replace_all(old="\n", new="\\n")
  .replace_all(old="\r", new="\\r")
}