///| Visualization export is explicitly geometry-only: require caller permission

///|
/// when metadata exists. MSH encode is the metadata-preserving exchange route.
fn Mesh::export_gate(self : Mesh, allow_loss : Bool) -> Unit raise {
  if !allow_loss &&
    (
      !self.fs.is_empty() ||
      !self.names.is_empty() ||
      !self.ents.is_empty() ||
      !self.unknown.is_empty() ||
      self.es.any(e => {
        !e.physical.is_empty() || !e.extra.is_empty() || e.entity != 0
      })
    ) {
    bad("geometry export omits metadata; require allow_loss")
  }
}

///| Legacy ASCII VTK unstructured grid, first-order cells only. Quadratic cells

///|
/// are rejected rather than silently reordered or linearized.
pub fn Mesh::vtk(self : Mesh, allow_loss? : Bool = false) -> String raise {
  self.export_gate(allow_loss)
  let w : Writer = { out: [], binary: false, little: true, width: 8, }
  w.text(
    "# vtk DataFile Version 3.0\nMoonGmsh geometry-only export\nASCII\nDATASET UNSTRUCTURED_GRID\nPOINTS \{self.ns.length()} double\n",
  )
  for n in self.ns {
    w.text("\{n.xyz[0]} \{n.xyz[1]} \{n.xyz[2]}\n")
  }
  let mut total = 0
  let types = []
  for e in self.es {
    let t = match e.kind {
      15 => 1
      1 => 3
      2 => 5
      3 => 9
      4 => 10
      5 => 12
      6 => 13
      7 => 14
      _ => {
        bad("VTK export supports first-order cells only")
        0
      }
    }
    types.push(t)
    total = total + e.nodes.length() + 1
  }
  w.text("CELLS \{self.es.length()} \{total}\n")
  for e in self.es {
    w.text("\{e.nodes.length()}")
    // Legacy vtkWedge (VTK 9.3, meshio 5.3.5) has the opposite base
    // orientation to Gmsh. Do not reuse the Gmsh connectivity verbatim.
    let nodes = if e.kind == 6 {
      [0, 2, 1, 3, 5, 4].map(i => e.nodes[i])
    } else {
      e.nodes
    }
    for n in nodes {
      w.text(" \{self.ni[n]}")
    }
    w.text("\n")
  }
  w.text("CELL_TYPES \{self.es.length()}\n")
  for t in types {
    w.text("\{t}\n")
  }
  @utf8.decode(Bytes::from_array(w.out))
}

///| OBJ lines/surface faces; volume meshes use reference-cell boundary facets at the

///|
/// highest dimension. Nonmanifold/incompatible polygon incidences are refused.
/// Reference-cell winding is not a geometric outward-normal certification for
/// inverted/warped cells. No orientation repair or self-intersection check.
pub fn Mesh::obj(self : Mesh, allow_loss? : Bool = false) -> String raise {
  self.export_gate(allow_loss)
  let w : Writer = { out: [], binary: false, little: true, width: 8, }
  w.text("# MoonGmsh geometry only; OBJ indices follow stored node order\n")
  for n in self.ns {
    w.text("v \{n.xyz[0]} \{n.xyz[1]} \{n.xyz[2]}\n")
  }
  let mut dim = 0
  for e in self.es {
    let d = element_shape(e.kind).0
    if d > dim {
      dim = d
    }
  }
  if dim == 3 {
    let t = self.topology()
    if !t.nonmanifold.is_empty() {
      bad("nonmanifold volume cannot be exported as an unambiguous boundary")
    }
    for f in t.boundary {
      w.text("f")
      for n in f.nodes {
        w.text(" \{self.ni[n]+1}")
      }
      w.text("\n")
    }
  } else {
    for e in self.es {
      let prefix = match e.kind {
        15 => "p"
        1 => "l"
        2 | 3 => "f"
        _ => {
          bad("OBJ export supports first-order surface cells only")
          ""
        }
      }
      w.text(prefix)
      for n in e.nodes {
        w.text(" \{self.ni[n]+1}")
      }
      w.text("\n")
    }
  }
  @utf8.decode(Bytes::from_array(w.out))
}