///|
fn finding(
  out : Array[Finding],
  code : String,
  id : String,
  message : String,
  other~ : String = "",
) -> Unit raise PlanError {
  if out.length() >= 4096 {
    reject(
      "FindingLimit", "layout produces more than 4096 violations; no partial valid verdict",
    )
  }
  out.push({ code, buffer: id, other, message, })
}

///|
/// Does not invoke the planner or trust a layout's size/lifetime metadata.
pub fn verify(
  problem : Problem,
  layout : Layout,
) -> Array[Finding] raise PlanError {
  validate(problem)
  if layout.placements.length() > 4096 {
    reject("LayoutLimit", "maximum 4096 placement records")
  }
  let out : Array[Finding] = []
  if layout.schema != 1 {
    finding(out, "Schema", "", "layout schema must be 1")
  }
  let buffers : Map[String, Buffer] = Map([])
  let capacities : Map[String, Int64] = Map([])
  for buffer in problem.buffers {
    buffers[buffer.id] = buffer
  }
  for bank in problem.banks {
    capacities[bank.id] = bank.capacity
  }
  let seen : Map[String, Bool] = Map([])
  let safe : Array[(Buffer, Placement)] = []
  for p in layout.placements {
    if seen.contains(p.id) {
      finding(out, "DuplicatePlacement", p.id, "buffer placed more than once")
      continue
    }
    seen[p.id] = true
    let buffer = match buffers.get(p.id) {
      Some(b) => b
      None => {
        finding(out, "UnknownBuffer", p.id, "placement has no source buffer")
        continue
      }
    }
    if !buffer.banks.contains(p.bank) || !capacities.contains(p.bank) {
      finding(out, "BankNotAllowed", p.id, "placement bank is not permitted")
      continue
    }
    if p.offset < 0L || p.offset > max_bytes - buffer.bytes {
      finding(out, "AddressRange", p.id, "offset outside address bounds")
      continue
    }
    if p.offset % buffer.alignment != 0L {
      finding(out, "Misaligned", p.id, "offset violates alignment")
    }
    if buffer.fixed_bank is Some(bank) &&
      (bank != p.bank || buffer.fixed_offset != Some(p.offset)) {
      finding(out, "FixedOffsetChanged", p.id, "fixed placement changed")
    }
    if p.offset + buffer.bytes > capacities[p.bank] {
      finding(out, "CapacityExceeded", p.id, "placement exceeds bank capacity")
    }
    safe.push((buffer, p))
  }
  for buffer in problem.buffers {
    if !seen.contains(buffer.id) {
      finding(out, "MissingPlacement", buffer.id, "source buffer not placed")
    }
  }
  for i in 0.. Array[BankUsage] raise PlanError {
  let findings = verify(problem, layout)
  if findings.any(f => f.code != "CapacityExceeded") {
    reject("InvalidLayout", "summary requires a structurally valid layout")
  }
  let by_id : Map[String, Buffer] = Map([])
  for buffer in problem.buffers {
    by_id[buffer.id] = buffer
  }
  let result : Array[BankUsage] = []
  for bank in problem.banks {
    let events : Array[(Int32, Int64)] = []
    let mut highwater = 0L
    for p in layout.placements {
      if p.bank != bank.id {
        continue
      }
      let b = by_id[p.id]
      let end = p.offset + b.bytes
      if end > highwater {
        highwater = end
      }
      events.push((b.first, b.bytes))
      events.push((b.last + 1, -b.bytes))
    }
    events.sort()
    let mut current = 0L
    let mut peak = 0L
    let mut step = 0
    let mut i = 0
    while i < events.length() {
      let time = events[i].0
      while i < events.length() && events[i].0 == time {
        current += events[i].1
        i += 1
      }
      if current > peak {
        peak = current
        step = time
      }
    }
    result.push({
      bank: bank.id,
      capacity: bank.capacity,
      highwater,
      peak_live_bytes: peak,
      peak_step: step,
      fits: highwater <= bank.capacity,
    })
  }
  result.sort_by((a, b) => id_compare(a.bank, b.bank))
  result
}

///|
/// C11 offset macros only. The caller owns correctly aligned bank storage.
pub fn c_header(problem : Problem, layout : Layout) -> String raise PlanError {
  if !verify(problem, layout).is_empty() {
    reject("InvalidLayout", "C export requires a verified in-budget layout")
  }
  let out = StringBuilder()
  out.write_string(
    "#ifndef MOON_ARENA_PLAN_H\n#define MOON_ARENA_PLAN_H\n#include \n",
  )
  let by_id : Map[String, Buffer] = Map([])
  for b in problem.buffers {
    by_id[b.id] = b
  }
  let ordered = layout.placements.copy()
  ordered.sort_by((a, b) => id_compare(a.id, b.id))
  for i, p in ordered {
    let b = by_id[p.id]
    out.write_string("/* " + p.id + " in " + p.bank + " */\n")
    out.write_string(
      "#define MP_BUFFER_" +
      i.to_string() +
      "_OFFSET UINT64_C(" +
      p.offset.to_string() +
      ")\n",
    )
    out.write_string(
      "#define MP_BUFFER_" +
      i.to_string() +
      "_BYTES UINT64_C(" +
      b.bytes.to_string() +
      ")\n",
    )
    out.write_string(
      "#define MP_BUFFER_" +
      i.to_string() +
      "_ALIGNMENT UINT64_C(" +
      b.alignment.to_string() +
      ")\n",
    )
  }
  out.write_string("#endif\n")
  out.to_string()
}