///|
pub suberror MeshError {
  Invalid(String)
} derive(Debug)

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

///|
pub(all) struct Node {
  tag : Int
  xyz : Array[Double]
  dim : Int
  entity : Int
  parameters : Array[Double]
} derive(ToJson)

///|
pub extend Node with ToJson::{to_json}

///|
pub(all) struct Element {
  tag : Int
  kind : Int
  nodes : Array[Int]
  entity : Int
  physical : Array[Int]
  extra : Array[Int]
} derive(ToJson)

///|
pub extend Element with ToJson::{to_json}

///|
pub(all) struct Entity {
  dim : Int
  tag : Int
  bounds : Array[Double]
  physical : Array[Int]
  boundary : Array[Int]
} derive(ToJson)

///|
pub extend Entity with ToJson::{to_json}

///|
pub(all) struct PhysicalName {
  dim : Int
  tag : Int
  name : String
} derive(ToJson)

///|
pub extend PhysicalName with ToJson::{to_json}

///|
pub(all) struct Datum {
  tag : Int
  values : Array[Double]
} derive(ToJson)

///|
pub extend Datum with ToJson::{to_json}

///|
pub(all) struct Field {
  location : String
  strings : Array[String]
  reals : Array[Double]
  integers : Array[Int]
  entries : Array[Datum]
} derive(ToJson)

///|
pub extend Field with ToJson::{to_json}

///|
pub(all) struct Section {
  name : String
  body : String
} derive(ToJson)

///|
pub extend Section with ToJson::{to_json}

///|
/// Owned snapshot. Public getters return deep copies; tag maps are never exposed.
pub struct Mesh {
  priv ns : Array[Node]
  priv es : Array[Element]
  priv ents : Array[Entity]
  priv names : Array[PhysicalName]
  priv fs : Array[Field]
  priv unknown : Array[Section]
  priv version : String
  priv ni : Map[Int, Int]
  priv ei : Map[Int, Int]
}

///|
fn bad(message : String) -> Unit raise MeshError {
  raise Invalid(message)
}

///|
fn finite(x : Double) -> Bool {
  x == x && x.abs() <= 1.7976931348623157e308
}

///|
fn positive(n : Int, what : String) -> Unit raise MeshError {
  if n <= 0 {
    bad("\{what}: tags must be positive signed 32-bit integers")
  }
}

///|
fn clean_name(s : String) -> Bool {
  !s.contains("\n") &&
  !s.contains("\r") &&
  !s.contains("\"") &&
  !s.contains("\\") &&
  !s.contains("\u0000")
}

///|
fn copy_node(n : Node) -> Node {
  { ..n, xyz: n.xyz.copy(), parameters: n.parameters.copy(), }
}

///|
fn copy_element(e : Element) -> Element {
  {
    ..e,
    nodes: e.nodes.copy(),
    physical: e.physical.copy(),
    extra: e.extra.copy(),
  }
}

///|
fn copy_entity(e : Entity) -> Entity {
  {
    ..e,
    bounds: e.bounds.copy(),
    physical: e.physical.copy(),
    boundary: e.boundary.copy(),
  }
}

///|
fn copy_field(f : Field) -> Field {
  {
    ..f,
    strings: f.strings.copy(),
    reals: f.reals.copy(),
    integers: f.integers.copy(),
    entries: f.entries.map(d => { ..d, values: d.values.copy(), }),
  }
}

///|
/// Gmsh's common first/second-order element IDs (manual MSH element table).
pub fn element_shape(kind : Int) -> (Int, Int) raise MeshError {
  match kind {
    1 => (1, 2)
    2 => (2, 3)
    3 => (2, 4)
    4 => (3, 4)
    5 => (3, 8)
    6 => (3, 6)
    7 => (3, 5)
    8 => (1, 3)
    9 => (2, 6)
    10 => (2, 9)
    11 => (3, 10)
    12 => (3, 27)
    13 => (3, 18)
    14 => (3, 14)
    15 => (0, 1)
    16 => (2, 8)
    17 => (3, 20)
    18 => (3, 15)
    19 => (3, 13)
    _ => {
      bad("unsupported element type \{kind}; common orders 1 and 2 only")
      (0, 0)
    }
  }
}

///| Portable resource contract: <=1M nodes/elements, 8M connectivity/data values.

///|
/// Geometry defects are retained for diagnostics; malformed references are not.
pub fn mesh(
  nodes : Array[Node],
  elements : Array[Element],
  entities? : Array[Entity] = [],
  names? : Array[PhysicalName] = [],
  fields? : Array[Field] = [],
  unknown? : Array[Section] = [],
  version? : String = "2.2",
) -> Mesh raise MeshError {
  if version != "2.2" && version != "4.1" {
    bad("version must be 2.2 or 4.1")
  }
  if nodes.length() > 1000000 ||
    elements.length() > 1000000 ||
    entities.length() > 1000000 ||
    fields.length() > 1024 ||
    unknown.length() > 1024 ||
    names.length() > 1000000 {
    bad("model resource limit")
  }
  let ni : Map[Int, Int] = Map([])
  let ei : Map[Int, Int] = Map([])
  let ent : Map[String, Entity] = Map([])
  for e in entities {
    positive(e.tag, "entity")
    let key = "\{e.dim}:\{e.tag}"
    if e.dim < 0 ||
      e.dim > 3 ||
      ent.contains(key) ||
      e.bounds.length() != (if e.dim == 0 { 3 } else { 6 }) {
      bad("invalid or duplicate entity")
    }
    if e.bounds.any(x => !finite(x)) ||
      e.physical.any(x => x <= 0) ||
      e.boundary.any(x => x == 0 || x == -2147483648) ||
      (e.dim == 0 && !e.boundary.is_empty()) {
      bad("invalid entity bounds/groups/boundary")
    }
    if e.dim > 0 {
      for j = 0; j < 3; j = j + 1 {
        if e.bounds[j] > e.bounds[j + 3] {
          bad("inverted entity bounding box")
        }
      }
    }
    ent[key] = e
  }
  for e in entities {
    for b in e.boundary {
      if !ent.contains("\{e.dim-1}:\{b.abs()}") {
        bad("missing entity boundary reference")
      }
    }
  }
  for i, n in nodes {
    positive(n.tag, "node")
    if ni.contains(n.tag) ||
      n.xyz.length() != 3 ||
      n.xyz.any(x => !finite(x)) ||
      n.parameters.any(x => !finite(x)) {
      bad("duplicate node or invalid coordinates")
    }
    if n.dim < -1 ||
      n.dim > 3 ||
      n.entity < 0 ||
      (n.dim == -1 && (n.entity != 0 || !entities.is_empty())) ||
      (!n.parameters.is_empty() && n.parameters.length() != n.dim) {
      bad("invalid node classification/parameters")
    }
    if n.dim >= 0 && (n.entity <= 0 || !ent.contains("\{n.dim}:\{n.entity}")) {
      bad("missing node entity")
    }
    ni[n.tag] = i
  }
  let mut links = 0
  for i, e in elements {
    positive(e.tag, "element")
    let (dim, width) = element_shape(e.kind)
    if ei.contains(e.tag) ||
      e.nodes.length() != width ||
      e.nodes.any(n => !ni.contains(n)) ||
      e.entity < 0 ||
      e.physical.any(p => p <= 0) {
      bad("invalid element tag, connectivity or group")
    }
    if !entities.is_empty() {
      match ent.get("\{dim}:\{e.entity}") {
        Some(entity) =>
          if entity.physical != e.physical {
            bad("element physical groups disagree with entity")
          }
        None => bad("missing element entity")
      }
    }
    links = links + width
    if links > 8000000 || e.extra.length() > 1024 {
      bad("connectivity/legacy tag limit")
    }
    ei[e.tag] = i
  }
  let seen : Map[String, Bool] = Map([])
  for n in names {
    let key = "\{n.dim}:\{n.tag}"
    if n.dim < 0 ||
      n.dim > 3 ||
      n.tag <= 0 ||
      !clean_name(n.name) ||
      n.name.length() > 127 ||
      seen.contains(key) {
      bad("invalid or duplicate physical name")
    }
    seen[key] = true
  }
  let mut values = 0
  for f in fields {
    if f.location != "NodeData" && f.location != "ElementData" {
      bad("field location must be NodeData or ElementData")
    }
    if f.strings.is_empty() ||
      f.strings.length() > 1024 ||
      f.strings.any(s => !clean_name(s) || s.length() > 4096) ||
      f.reals.length() > 1024 ||
      f.reals.any(x => !finite(x)) ||
      f.integers.length() < 3 ||
      f.integers.length() > 1024 {
      bad("invalid data header")
    }
    let nc = f.integers[1]
    if (nc != 1 && nc != 3 && nc != 9) ||
      f.integers[2] != f.entries.length() ||
      f.entries.length() > 1000000 {
      bad("data components/count must be 1,3,9 and match entries")
    }
    let ids : Map[Int, Bool] = Map([])
    for d in f.entries {
      if ids.contains(d.tag) ||
        !(if f.location == "NodeData" {
          ni.contains(d.tag)
        } else {
          ei.contains(d.tag)
        }) ||
        d.values.length() != nc ||
        d.values.any(x => !finite(x)) {
        bad("invalid field data or missing tag")
      }
      ids[d.tag] = true
      values = values + nc
      if values > 8000000 {
        bad("data value limit")
      }
    }
  }
  for s in unknown {
    if s.name.is_empty() ||
      s.name.length() > 128 ||
      !s.name
      .iter()
      .all(c => {
        (c >= 'A' && c <= 'Z') ||
        (c >= 'a' && c <= 'z') ||
        (c >= '0' && c <= '9') ||
        c == '_'
      }) ||
      s.name.has_prefix("End") ||
      [
        "MeshFormat", "Nodes", "Elements", "Entities", "PhysicalNames", "NodeData",
        "ElementData",
      ].contains(s.name) ||
      s.body.contains("$End\{s.name}") ||
      s.body.contains("\u0000") {
      bad("unsafe unknown section")
    }
  }
  {
    ns: nodes.map(copy_node),
    es: elements.map(copy_element),
    ents: entities.map(copy_entity),
    names: names.copy(),
    fs: fields.map(copy_field),
    unknown: unknown.copy(),
    version,
    ni,
    ei,
  }
}

///|
pub fn Mesh::nodes(self : Mesh) -> Array[Node] {
  self.ns.map(copy_node)
}

///|
pub fn Mesh::elements(self : Mesh) -> Array[Element] {
  self.es.map(copy_element)
}

///|
pub fn Mesh::entities(self : Mesh) -> Array[Entity] {
  self.ents.map(copy_entity)
}

///|
pub fn Mesh::physical_names(self : Mesh) -> Array[PhysicalName] {
  self.names.copy()
}

///|
pub fn Mesh::fields(self : Mesh) -> Array[Field] {
  self.fs.map(copy_field)
}

///|
pub fn Mesh::unknown_sections(self : Mesh) -> Array[Section] {
  self.unknown.copy()
}

///|
pub fn Mesh::source_version(self : Mesh) -> String {
  self.version
}

///|
pub fn Mesh::without_unknown(self : Mesh) -> Mesh {
  { ..self, unknown: [], }
}