///|
pub(all) struct BoundaryBatchResult {
  alive : Array[Particle]
  absorbed : Int
  reflected : Int
  wrapped : Int
} derive(Debug, ToJson)

///|
pub fn apply_particle_boundary_batch(
  grid : Grid1D,
  boundary : Boundary1D,
  particles : ArrayView[Particle],
) -> BoundaryBatchResult {
  let alive : Array[Particle] = []
  let mut absorbed = 0
  let mut reflected = 0
  let mut wrapped = 0
  for particle in particles {
    let result = apply_boundary(grid, boundary, particle.x, particle.v)
    if result.alive {
      if boundary == Reflecting && result.v != particle.v {
        reflected = reflected + 1
      }
      if boundary == Periodic && result.x != particle.x {
        wrapped = wrapped + 1
      }
      alive.push(
        Particle::new(
          x=result.x,
          v=result.v,
          weight=particle.weight,
          charge=particle.charge,
          mass=particle.mass,
        ),
      )
    } else {
      absorbed = absorbed + 1
    }
  }
  { alive, absorbed, reflected, wrapped }
}

///|
pub fn boundary_loss_fraction(
  result : BoundaryBatchResult,
  original_count : Int,
) -> Double {
  safe_ratio(result.absorbed.to_double(), original_count.to_double(), 0.0)
}

///|
pub fn boundary_reflection_fraction(
  result : BoundaryBatchResult,
  original_count : Int,
) -> Double {
  safe_ratio(result.reflected.to_double(), original_count.to_double(), 0.0)
}

///|
pub fn boundary_wrap_fraction(
  result : BoundaryBatchResult,
  original_count : Int,
) -> Double {
  safe_ratio(result.wrapped.to_double(), original_count.to_double(), 0.0)
}

///|
pub fn reflect_position(grid : Grid1D, x : Double) -> Double {
  let mut result = x
  let mut passes = 0
  while (result < 0.0 || result >= grid.length) && passes < 16 {
    if result < 0.0 {
      result = -result
    } else {
      result = 2.0 * grid.length - result
    }
    passes = passes + 1
  }
  clamp(result, 0.0, grid.length)
}

///|
pub fn apply_reflecting_position(
  grid : Grid1D,
  x : Double,
  v : Double,
) -> (Double, Double) {
  let result = apply_boundary(grid, Reflecting, x, v)
  (result.x, result.v)
}

///|
pub fn boundary_valid(grid : Grid1D, particles : ArrayView[Particle]) -> Bool {
  let mut valid = true
  for particle in particles {
    if particle.x < 0.0 || particle.x >= grid.length {
      valid = false
    }
  }
  valid
}

///|
pub fn boundary_positions(
  grid : Grid1D,
  particles : ArrayView[Particle],
) -> Array[Double] {
  particles.map(fn(particle) { grid.wrap(particle.x) })
}

///|
pub fn boundary_velocity_flux(
  grid : Grid1D,
  particles : ArrayView[Particle],
) -> Double {
  let mut flux = 0.0
  for particle in particles {
    if particle.x < 0.0 || particle.x >= grid.length {
      flux = flux + particle.v * particle.weight
    }
  }
  flux
}