///| Tokenization never skips across a binary payload boundary. Header line endings

///|
/// are consumed exactly once; a binary tag may legitimately begin with 0x0A.
priv struct Cursor {
  data : Bytes
  mut p : Int
  mut binary : Bool
  mut little : Bool
  mut width : Int
}

///|
fn Cursor::need(c : Cursor, n : Int) -> Unit raise MeshError {
  if n < 0 || n > c.data.length() - c.p {
    bad("truncated MSH at byte \{c.p}")
  }
}

///|
fn Cursor::line(c : Cursor) -> String raise {
  let start = c.p
  while c.p < c.data.length() && c.data[c.p] != 10 {
    c.p = c.p + 1
    if c.p - start > 1048576 {
      bad("text line exceeds 1 MiB")
    }
  }
  let end = if c.p > start && c.data[c.p - 1] == 13 { c.p - 1 } else { c.p }
  if c.p < c.data.length() {
    c.p = c.p + 1
  }
  @utf8.decode(c.data[start:end], ignore_bom=false)
}

///|
fn white(b : Byte) -> Bool {
  b == 32 || b == 9 || b == 10 || b == 13
}

///|
fn Cursor::word(c : Cursor) -> String raise {
  while c.p < c.data.length() && white(c.data[c.p]) {
    c.p = c.p + 1
  }
  let start = c.p
  while c.p < c.data.length() && !white(c.data[c.p]) {
    c.p = c.p + 1
    if c.p - start > 128 {
      bad("numeric token too long")
    }
  }
  if start == c.p {
    bad("expected token at end of input")
  }
  @utf8.decode(c.data[start:c.p])
}

///|
fn decimal(s : String) -> Int raise {
  if s.is_empty() {
    bad("empty integer")
  }
  let mut digits = 0
  for i, ch in s.iter().to_array() {
    if ch >= '0' && ch <= '9' {
      digits = digits + 1
    } else if i != 0 || (ch != '-' && ch != '+') {
      bad("invalid decimal integer: \{s}")
    }
  }
  if digits == 0 {
    bad("invalid integer")
  }
  @string.parse_int(s)
}

///|
fn Cursor::ai(c : Cursor) -> Int raise {
  decimal(c.word())
}

///|
fn Cursor::ar(c : Cursor) -> Double raise {
  let s = c.word()
  if !s
    .iter()
    .all(ch => {
      (ch >= '0' && ch <= '9') ||
      ch == '+' ||
      ch == '-' ||
      ch == '.' ||
      ch == 'e' ||
      ch == 'E'
    }) {
    bad("invalid real token")
  }
  let x = @string.parse_double(s)
  if !finite(x) {
    bad("nonfinite real value")
  }
  x
}

///|
fn Cursor::eol(c : Cursor) -> Unit raise {
  // Only horizontal whitespace is permitted before the one required line ending.
  while c.p < c.data.length() && (c.data[c.p] == 32 || c.data[c.p] == 9) {
    c.p = c.p + 1
  }
  if c.p < c.data.length() && c.data[c.p] == 13 {
    c.p = c.p + 1
  }
  c.need(1)
  if c.data[c.p] != 10 {
    bad("expected line ending at byte \{c.p}")
  }
  c.p = c.p + 1
}

///|
fn Cursor::bits(c : Cursor, n : Int) -> UInt64 raise {
  c.need(n)
  let mut value = 0UL
  for i = 0; i < n; i = i + 1 {
    let at = if c.little { c.p + n - 1 - i } else { c.p + i }
    value = (value << 8) | c.data[at].to_uint64()
  }
  c.p = c.p + n
  value
}

///|
fn Cursor::integer(c : Cursor) -> Int raise {
  if c.binary {
    c.bits(4).to_uint().reinterpret_as_int()
  } else {
    c.ai()
  }
}

///|
fn Cursor::size(c : Cursor) -> Int raise {
  if !c.binary {
    c.ai()
  } else {
    let n = c.bits(c.width)
    if n > 2147483647UL {
      bad("64-bit tag/count exceeds portable signed-32-bit limit")
    }
    n.to_int()
  }
}

///|
fn Cursor::real(c : Cursor) -> Double raise {
  if !c.binary {
    c.ar()
  } else {
    let x = c.bits(8).reinterpret_as_double()
    if !finite(x) {
      bad("nonfinite binary value")
    }
    x
  }
}

///|
fn count(n : Int, limit : Int) -> Int raise {
  if n < 0 || n > limit {
    bad("negative or excessive section count: \{n}")
  }
  n
}

///|
fn Cursor::end_section(
  c : Cursor,
  name : String,
  binary_payload : Bool,
) -> Unit raise {
  if binary_payload {
    c.eol()
  }
  let marker = c.word()
  if marker != "$End\{name}" {
    bad("expected $End\{name}, got \{marker}")
  }
  if c.p < c.data.length() {
    c.eol()
  }
}

///|
fn Cursor::quoted(c : Cursor) -> String raise {
  while c.p < c.data.length() && white(c.data[c.p]) {
    c.p = c.p + 1
  }
  c.need(1)
  if c.data[c.p] != 34 {
    bad("expected quoted string")
  }
  c.p = c.p + 1
  let start = c.p
  while c.p < c.data.length() && c.data[c.p] != 34 {
    if c.p - start > 4096 ||
      c.data[c.p] == 10 ||
      c.data[c.p] == 13 ||
      c.data[c.p] == 92 {
      bad("unsupported quoted string or escape")
    }
    c.p = c.p + 1
  }
  c.need(1)
  let value = @utf8.decode(c.data[start:c.p])
  c.p = c.p + 1
  value
}

///| Reject unknown binary sections: searching for an ASCII delimiter inside binary

///|
/// data is unsafe. Unknown ASCII section bodies are preserved as opaque text.
pub fn decode(data : Bytes) -> Mesh raise {
  if data.length() > 134217728 {
    bad("MSH file exceeds 128 MiB")
  }
  let c : Cursor = { data, p: 0, binary: false, little: true, width: 8, }
  if c.line() != "$MeshFormat" {
    bad("MSH must start with $MeshFormat")
  }
  let version = c.word()
  let ft = c.ai()
  let width = c.ai()
  c.eol()
  if (version != "2.2" && version != "4.1") ||
    (ft != 0 && ft != 1) ||
    (width != 8 && !(version == "4.1" && width == 4)) {
    bad("supported formats: MSH 2.2/4.1, ASCII/binary, size_t 4/8")
  }
  c.width = width
  c.binary = ft == 1
  if c.binary {
    let magic = c.bits(4)
    if magic == 16777216UL {
      c.little = false
    } else if magic != 1UL {
      bad("invalid endian sentinel")
    }
  }
  c.end_section("MeshFormat", c.binary)
  let ns : Array[Node] = []
  let es : Array[Element] = []
  let ents : Array[Entity] = []
  let names : Array[PhysicalName] = []
  let fields : Array[Field] = []
  let unknown : Array[Section] = []
  let seen : Map[String, Bool] = Map([])
  while c.p < data.length() {
    let line = c.line().trim().to_owned()
    if line.is_empty() {
      continue
    }
    if !line.has_prefix("$") || line.has_prefix("$End") {
      bad("expected section marker")
    }
    let name = line[1:].to_owned()
    if name == "MeshFormat" {
      bad("duplicate MeshFormat")
    }
    if ["Nodes", "Elements", "Entities", "PhysicalNames"].contains(name) {
      if seen.contains(name) {
        bad("duplicate core section: \{name}")
      }
      seen[name] = true
    }
    match name {
      "PhysicalNames" => {
        let n = count(c.ai(), 1000000)
        for _ in 0.. {
        if version != "4.1" {
          bad("Entities requires 4.1")
        }
        let counts = [c.size(), c.size(), c.size(), c.size()]
        for dim = 0; dim < 4; dim = dim + 1 {
          let n = count(counts[dim], 1000000 - ents.length())
          for _ in 0..
        if version == "2.2" {
          let n = count(c.ai(), 1000000)
          c.eol()
          for _ in 0.. 0)
        } else {
          let blocks = count(c.size(), 1000000)
          let total = count(c.size(), 1000000)
          let lo = c.size()
          let hi = c.size()
          for _ in 0.. 3 || (param != 0 && param != 1) {
              bad("invalid node block dimension/parametric flag")
            }
            let n = count(c.size(), total - ns.length())
            let tags = []
            for _ in 0.. n.tag), total, lo, hi)
          c.end_section(name, c.binary)
        }
      "Elements" =>
        if version == "2.2" {
          let total = count(c.ai(), 1000000)
          c.eol()
          while es.length() < total {
            let first = c.integer()
            let kind = if c.binary { first } else { c.integer() }
            let n = if c.binary {
              count(c.integer(), total - es.length())
            } else {
              1
            }
            if n == 0 {
              bad("empty legacy binary element block")
            }
            let nt = count(c.integer(), 1024)
            let (_, width) = element_shape(kind)
            for _ in 0.. x == -2147483648) {
                bad("legacy tag out of range")
              }
              let nodes = []
              for _ in 0.. 0 && tags[0] != 0 { [tags[0]] } else { [] }
              let entity = if nt > 1 { tags[1] } else { 0 }
              let extra = if nt > 2 { tags[2:].to_owned() } else { [] }
              es.push({ tag, kind, nodes, entity, physical, extra, })
            }
          }
          c.end_section(name, c.binary && total > 0)
        } else {
          let blocks = count(c.size(), 1000000)
          let total = count(c.size(), 1000000)
          let lo = c.size()
          let hi = c.size()
          for _ in 0.. e.tag), total, lo, hi)
          c.end_section(name, c.binary)
        }
      "NodeData" | "ElementData" => {
        if fields.length() >= 1024 {
          bad("too many field frames")
        }
        let n = count(c.ai(), 1024)
        let strings = []
        for _ in 0.. 0)
      }
      _ => {
        if c.binary {
          bad("cannot safely preserve unknown binary section: \{name}")
        }
        if unknown.length() >= 1024 {
          bad("too many unknown sections")
        }
        let body = StringBuilder()
        let mut ended = false
        while c.p < data.length() {
          let s = c.line()
          if s == "$End\{name}" {
            ended = true
            break
          }
          body.write_string(s)
          body.write_string("\n")
        }
        if !ended {
          bad("unterminated unknown section")
        }
        unknown.push({ name, body: body.to_string(), })
      }
    }
  }
  if !seen.contains("Nodes") || !seen.contains("Elements") {
    bad("Nodes and Elements sections required")
  }
  if version == "4.1" {
    let groups : Map[String, Array[Int]] = Map([])
    for e in ents {
      groups["\{e.dim}:\{e.tag}"] = e.physical
    }
    for i, e in es {
      let (dim, _) = element_shape(e.kind)
      es[i] = { ..e, physical: groups.get("\{dim}:\{e.entity}").unwrap_or([]), }
    }
    // 4.1 permits files without Entities. Synthesize classification bounds with no
    // physical claims, only when the entire Entities section is absent.
    if !seen.contains("Entities") {
      synthesize_entities(ns, es, ents)
    }
  }
  mesh(ns, es, entities=ents, names~, fields~, unknown~, version~)
}

///|
fn check_range(
  tags : Array[Int],
  total : Int,
  lo : Int,
  hi : Int,
) -> Unit raise {
  if tags.length() != total {
    bad("section count mismatch")
  }
  let mut min = 2147483647
  let mut max = 0
  for tag in tags {
    if tag < min {
      min = tag
    }
    if tag > max {
      max = tag
    }
  }
  if total == 0 {
    if lo != 0 || hi != 0 {
      bad("empty range must be 0 0")
    }
  } else if lo != min || hi != max {
    bad("section min/max tag mismatch")
  }
}

///|
fn synthesize_entities(
  ns : Array[Node],
  es : Array[Element],
  out : Array[Entity],
) -> Unit raise {
  let groups : Map[String, (Int, Int, Array[Int], Array[Double])] = Map([])
  let index : Map[Int, Node] = Map([])
  for n in ns {
    index[n.tag] = n
  }
  for n in ns {
    if n.dim >= 0 && n.entity > 0 {
      let key = "\{n.dim}:\{n.entity}"
      let bounds = match groups.get(key) {
        Some((_, _, _, b)) => b
        None => {
          let b = [n.xyz[0], n.xyz[1], n.xyz[2], n.xyz[0], n.xyz[1], n.xyz[2]]
          groups[key] = (n.dim, n.entity, [], b)
          b
        }
      }
      grow(bounds, n.xyz)
    }
  }
  for e in es {
    let (dim, _) = element_shape(e.kind)
    if e.entity <= 0 {
      bad("entity synthesis requires positive entity tags")
    }
    let key = "\{dim}:\{e.entity}"
    let first = match index.get(e.nodes[0]) {
      Some(n) => n
      None => {
        bad("missing node during entity synthesis")
        { tag: 1, xyz: [0.0, 0.0, 0.0], dim: -1, entity: 0, parameters: [], }
      }
    }
    let bounds = match groups.get(key) {
      Some((_, _, phys, b)) => {
        if phys != e.physical && !phys.is_empty() {
          bad("same entity has inconsistent physical groups")
        }
        groups[key] = (dim, e.entity, e.physical, b)
        b
      }
      None => {
        let b = [
          first.xyz[0],
          first.xyz[1],
          first.xyz[2],
          first.xyz[0],
          first.xyz[1],
          first.xyz[2],
        ]
        groups[key] = (dim, e.entity, e.physical, b)
        b
      }
    }
    for tag in e.nodes {
      match index.get(tag) {
        Some(n) => grow(bounds, n.xyz)
        None => bad("missing node")
      }
    }
  }
  let keys = groups.keys().to_array()
  keys.sort()
  for key in keys {
    let (dim, tag, physical, b) = groups[key]
    out.push({
      dim,
      tag,
      physical,
      bounds: if dim == 0 {
        b[:3].to_owned()
      } else {
        b
      },
      boundary: [],
    })
  }
}

///|
fn grow(b : Array[Double], xyz : Array[Double]) -> Unit {
  for j = 0; j < 3; j = j + 1 {
    if xyz[j] < b[j] {
      b[j] = xyz[j]
    }
    if xyz[j] > b[j + 3] {
      b[j + 3] = xyz[j]
    }
  }
}