///|
pub struct Diagnostic {
  code : String
  node : String
  line : Int32
  message : String
} derive(Eq, @debug.Debug)

///|
pub extend Diagnostic with Eq::{equal, not_equal}

///|
pub extend Diagnostic with @debug.Debug::{to_repr}

///|
fn successors(story : Story, pc : Int32) -> Array[Int32] {
  let out = []
  match story.code[pc].op {
    End(_) => ()
    Jump(t) => if story.labels.get(t) is Some(dest) { out.push(dest) }
    Branch(_, t) | Pick(_, _, t, _) => {
      if story.labels.get(t) is Some(dest) {
        out.push(dest)
      }
      match story.code[pc].op {
        Branch(_, _) => out.push(pc + 1)
        _ =>
          if pc + 1 < story.code.length() &&
            story.code[pc + 1].node == story.code[pc].node {
            // Later alternatives in a contiguous choice group are also possible.
            out.push(pc + 1)
          }
      }
    }
    _ => out.push(pc + 1)
  }
  out
}

///|
fn walk_graph(edges : Array[Array[Int32]], seeds : Array[Int32]) -> Array[Bool] {
  let seen = Array::make(edges.length(), false)
  let queue = seeds.copy()
  for seed in seeds {
    seen[seed] = true
  }
  let mut head = 0
  while head < queue.length() {
    let pc = queue[head]
    head = head + 1
    for next in edges[pc] {
      if !seen[next] {
        seen[next] = true
        queue.push(next)
      }
    }
  }
  seen
}

///|
/// Structural analysis ignores condition truth and does not prove playability.
pub fn Story::diagnostics(self : Story) -> Array[Diagnostic] {
  let edges : Array[Array[Int32]] = []
  let reverse : Array[Array[Int32]] = []
  let endings = []
  for i = 0; i < self.code.length(); i = i + 1 {
    edges.push(successors(self, i))
    reverse.push([])
    if self.code[i].op is End(_) {
      endings.push(i)
    }
  }
  for i = 0; i < edges.length(); i = i + 1 {
    for dest in edges[i] {
      reverse[dest].push(i)
    }
  }
  let reachable = walk_graph(edges, [0])
  let can_end = walk_graph(reverse, endings)
  let out = []
  for node in self.order {
    if self.labels.get(node) is Some(pc) {
      if !reachable[pc] {
        out.push({
          code: "unreachable",
          node,
          line: self.code[pc].line,
          message: "node has no structural path from entry",
        })
      }
      if !can_end[pc] {
        out.push({
          code: "no_end_path",
          node,
          line: self.code[pc].line,
          message: "node has no structural path to an end instruction",
        })
      }
    }
  }
  out
}