///|
pub struct Mesh {
  priv ne : Int
  priv nn : Int
  priv eptr : FixedArray[Int]
  priv eind : FixedArray[Int]
}

///|
pub(all) struct MeshPartitionResult {
  objval : Int
  epart : FixedArray[Int]
  npart : FixedArray[Int]
}

///|
pub fn Mesh::new(
  ne : Int,
  nn : Int,
  eptr : FixedArray[Int],
  eind : FixedArray[Int],
) -> Mesh raise MetisError {
  let mesh = { ne, nn, eptr, eind }
  mesh.validate()
  mesh
}

///|
pub fn Mesh::ne(self : Mesh) -> Int {
  self.ne
}

///|
pub fn Mesh::nn(self : Mesh) -> Int {
  self.nn
}

///|
pub fn Mesh::eptr(self : Mesh) -> FixedArray[Int] {
  self.eptr
}

///|
pub fn Mesh::eind(self : Mesh) -> FixedArray[Int] {
  self.eind
}

///|
fn Mesh::validate(self : Mesh) -> Unit raise MetisError {
  if self.ne <= 0 || self.nn <= 0 {
    raise InvalidMesh
  }
  if self.eptr.length() != self.ne + 1 {
    raise InvalidMesh
  }
  if self.eptr.length() == 0 || self.eptr[0] != 0 {
    raise InvalidMesh
  }
  for i in 0.. self.eptr[i + 1] {
      raise InvalidMesh
    }
  }
  if self.eptr[self.ne] != self.eind.length() {
    raise InvalidMesh
  }
  for node in self.eind {
    if node < 0 || node >= self.nn {
      raise InvalidMesh
    }
  }
}

///|
pub fn mesh_to_dual(
  mesh : Mesh,
  ncommon? : Int = 1,
) -> CsrGraph raise MetisError {
  mesh.validate()
  if ncommon < 1 {
    raise InvalidOptions
  }
  let result = metis_mesh_to_dual_ffi(
    mesh.ne,
    mesh.nn,
    mesh.eptr,
    mesh.eind,
    ncommon,
    0,
  )
  raise_status(native_graph_result_status(result))
  CsrGraph::new(
    mesh.ne,
    native_graph_result_xadj(result),
    native_graph_result_adjncy(result),
  )
}

///|
pub fn mesh_to_nodal(mesh : Mesh) -> CsrGraph raise MetisError {
  mesh.validate()
  let result = metis_mesh_to_nodal_ffi(
    mesh.ne,
    mesh.nn,
    mesh.eptr,
    mesh.eind,
    0,
  )
  raise_status(native_graph_result_status(result))
  CsrGraph::new(
    mesh.nn,
    native_graph_result_xadj(result),
    native_graph_result_adjncy(result),
  )
}

///|
fn validate_mesh_partition_inputs(
  mesh : Mesh,
  nparts : Int,
  target_partition_weights : FixedArray[Float]?,
) -> Unit raise MetisError {
  mesh.validate()
  if nparts <= 0 {
    raise InvalidOptions
  }
  match target_partition_weights {
    None => ()
    Some(tpwgts) => if tpwgts.length() != nparts { raise InvalidOptions }
  }
}

///|
fn validate_optional_int_array(
  value : FixedArray[Int]?,
  expected_length : Int,
) -> Unit raise MetisError {
  match value {
    None => ()
    Some(value) => if value.length() != expected_length { raise InvalidOptions }
  }
}

///|
pub fn part_mesh_nodal(
  mesh : Mesh,
  nparts : Int,
  options? : Options,
  target_partition_weights? : FixedArray[Float],
  node_weights? : FixedArray[Int],
  node_sizes? : FixedArray[Int],
) -> MeshPartitionResult raise MetisError {
  validate_mesh_partition_inputs(mesh, nparts, target_partition_weights)
  validate_optional_int_array(node_weights, mesh.nn)
  validate_optional_int_array(node_sizes, mesh.nn)
  let options = option_or_default(options)
  let objval = Ref(0)
  let epart = FixedArray::make(mesh.ne, 0)
  let npart = FixedArray::make(mesh.nn, 0)
  let status = metis_part_mesh_nodal_ffi(
    mesh.ne,
    mesh.nn,
    mesh.eptr,
    mesh.eind,
    int_array_or_empty(node_weights),
    int_array_or_empty(node_sizes),
    nparts,
    float_array_or_empty(target_partition_weights),
    options.raw(),
    objval,
    epart,
    npart,
  )
  raise_status(status)
  { objval: objval.val, epart, npart }
}

///|
pub fn part_mesh_dual(
  mesh : Mesh,
  nparts : Int,
  ncommon? : Int = 1,
  options? : Options,
  target_partition_weights? : FixedArray[Float],
  element_weights? : FixedArray[Int],
  element_sizes? : FixedArray[Int],
) -> MeshPartitionResult raise MetisError {
  validate_mesh_partition_inputs(mesh, nparts, target_partition_weights)
  validate_optional_int_array(element_weights, mesh.ne)
  validate_optional_int_array(element_sizes, mesh.ne)
  if ncommon < 1 {
    raise InvalidOptions
  }
  let options = option_or_default(options)
  let objval = Ref(0)
  let epart = FixedArray::make(mesh.ne, 0)
  let npart = FixedArray::make(mesh.nn, 0)
  let status = metis_part_mesh_dual_ffi(
    mesh.ne,
    mesh.nn,
    mesh.eptr,
    mesh.eind,
    int_array_or_empty(element_weights),
    int_array_or_empty(element_sizes),
    ncommon,
    nparts,
    float_array_or_empty(target_partition_weights),
    options.raw(),
    objval,
    epart,
    npart,
  )
  raise_status(status)
  { objval: objval.val, epart, npart }
}