///|
#derive(Debug, Eq)
pub(all) struct SourceLocation {
  script_id : Int32
  line : Int32
  column : Int32
}

///|
pub extend SourceLocation with Debug::{to_repr}

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

///|
pub(all) struct AllocationFrame {
  function_id : String
  name : String
  script_name : String
  script_id : Int32
  line : Int32
  column : Int32
}

///|
priv struct AllocationTrace {
  parent : Int32
  function_index : Int32
  count : Int64
  bytes : Int64
}

///|
priv struct SourceIndex {
  origins : Array[Int32]
  locations : Map[Int32, SourceLocation]
  functions : Array[AllocationFrame]
  traces : Map[Int32, AllocationTrace]
}

///|
fn empty_sources(count : Int32) -> SourceIndex {
  {
    origins: Array::make(count, 0),
    locations: Map([]),
    functions: [],
    traces: Map([]),
  }
}

///|
fn signed_index(v : Json, context : String) -> Int32 raise HeapError {
  guard v is Number(_, ..) else {
    raise Invalid("\{context}: expected integer")
  }
  @string.parse_int(v.stringify()) catch {
    _ => raise Invalid("\{context}: invalid integer")
  }
}

///|
fn safe_bytes(v : Json, context : String) -> Int64 raise HeapError {
  let n = unsigned(v, context)
  guard n <= 9007199254740991UL else {
    raise Limit("\{context}: exceeds exact byte range")
  }
  n.reinterpret_as_int64()
}

///|
priv struct TraceWork {
  items : Array[Json]
  parent : Int32
}

///|
fn parse_sources(
  metadata : Map[String, Json],
  meta : Json,
  fields : Array[String],
  raw_nodes : Array[UInt64],
  strings : Array[String],
  count : Int32,
) -> SourceIndex raise HeapError {
  let source = empty_sources(count)
  for col in 0.. {
      let records = array(v, "locations")
      if !records.is_empty() {
        let fs = string_array(field(meta, "location_fields"), "location_fields")
        unique_fields(fs)
        let at = offset(fs, "object_index")
        let script = offset(fs, "script_id")
        let line = offset(fs, "line")
        let column = offset(fs, "column")
        guard records.length() % fs.length() == 0 else {
          raise Invalid("location record width mismatch")
        }
        for row = 0; row < records.length(); row = row + fs.length() {
          let raw = integer(records[row + at], "location object_index")
          guard raw % fields.length() == 0 && raw / fields.length() < count else {
            raise Invalid("location target out of range")
          }
          let node = raw / fields.length()
          guard !source.locations.contains(node) else {
            raise Invalid("duplicate object location")
          }
          source.locations[node] = {
            script_id: signed_index(records[row + script], "script_id"),
            line: signed_index(records[row + line], "line"),
            column: signed_index(records[row + column], "column"),
          }
        }
      }
    }
    None => ()
  }
  match metadata.get("trace_function_infos") {
    Some(v) => {
      let records = array(v, "trace_function_infos")
      if !records.is_empty() {
        let fs = string_array(
          field(meta, "trace_function_info_fields"),
          "trace_function_info_fields",
        )
        unique_fields(fs)
        let fid = offset(fs, "function_id")
        let name = offset(fs, "name")
        let script = offset(fs, "script_name")
        let sid = offset(fs, "script_id")
        let line = offset(fs, "line")
        let column = offset(fs, "column")
        guard records.length() % fs.length() == 0 else {
          raise Invalid("function info width mismatch")
        }
        for row = 0; row < records.length(); row = row + fs.length() {
          source.functions.push({
            function_id: unsigned(records[row + fid], "function_id").to_string(),
            name: indexed(
              strings,
              integer(records[row + name], "function name"),
              "function name",
            ),
            script_name: indexed(
              strings,
              integer(records[row + script], "script name"),
              "script name",
            ),
            script_id: signed_index(records[row + sid], "script_id"),
            line: signed_index(records[row + line], "line"),
            column: signed_index(records[row + column], "column"),
          })
        }
      }
    }
    None => ()
  }
  match metadata.get("trace_tree") {
    Some(v) => {
      let first = array(v, "trace_tree")
      if !first.is_empty() {
        let fs = string_array(
          field(meta, "trace_node_fields"),
          "trace_node_fields",
        )
        unique_fields(fs)
        let tid = offset(fs, "id")
        let fi = offset(fs, "function_info_index")
        let ct = offset(fs, "count")
        let sz = offset(fs, "size")
        let child = offset(fs, "children")
        let work : Array[TraceWork] = [{ items: first, parent: 0, }]
        let mut cursor = 0
        while cursor < work.length() {
          let part = work[cursor]
          cursor += 1
          guard part.items.length() % fs.length() == 0 else {
            raise Invalid("trace node width mismatch")
          }
          for row = 0; row < part.items.length(); row = row + fs.length() {
            let id = integer(part.items[row + tid], "trace id")
            let index = integer(part.items[row + fi], "function info index")
            guard id > 0 &&
              !source.traces.contains(id) &&
              index < source.functions.length() else {
              raise Invalid("invalid or duplicate allocation trace")
            }
            source.traces[id] = {
              parent: part.parent,
              function_index: index,
              count: safe_bytes(part.items[row + ct], "allocation count"),
              bytes: safe_bytes(part.items[row + sz], "allocation size"),
            }
            let children = array(part.items[row + child], "trace children")
            if !children.is_empty() {
              work.push({ items: children, parent: id, })
            }
          }
        }
      }
    }
    None => ()
  }
  for id in source.origins {
    guard id == 0 || source.traces.contains(id) else {
      raise Invalid("node references missing allocation trace")
    }
  }
  source
}

///|
/// Object locations use V8's zero-based line/column coordinates.
pub fn Snapshot::source_location(
  self : Snapshot,
  node : Int32,
) -> SourceLocation? raise HeapError {
  ignore(self.node(node))
  self.source.locations.get(node)
}

///|
/// Leaf allocation site first, followed by callers. Frames preserve V8 values.
pub fn Snapshot::allocation_stack(
  self : Snapshot,
  node : Int32,
) -> Array[AllocationFrame] raise HeapError {
  ignore(self.node(node))
  let result : Array[AllocationFrame] = []
  let mut id = self.source.origins[node]
  while id != 0 {
    let t = self.source.traces.get(id).unwrap()
    result.push(self.source.functions[t.function_index])
    id = t.parent
  }
  result
}

///|
/// Allocations recorded at this trace site, not this object's current size.
pub fn Snapshot::allocation_totals(
  self : Snapshot,
  node : Int32,
) -> (Int64, Int64)? raise HeapError {
  ignore(self.node(node))
  self.source.traces.get(self.source.origins[node]).map(t => (t.count, t.bytes))
}