///|
/// Explicit completion state; unchecked and failed are never a zero count.
pub(all) enum CrossingCheck {
  NotChecked
  Checked(Int32)
  Indeterminate(Int32, Int32)
  Failed(String)
}

///|
/// Scalar-only analysis. Local feasibility excludes topology by definition.
pub(all) struct Metrics {
  pressure_sample_index : Int32
  offset_factor_sample_index : Int32
  max_pressure : Double
  pressure_angle : Double
  min_offset_factor : Double
  min_offset_factor_angle : Double
  max_velocity : Double
  max_acceleration : Double
  max_jerk : Double
  jerk_jumps : Int32
  sampled_local_feasible : Bool
  sample_count : Int32
  nominal_resolution : Int32
}

///|
/// Sampled acceptance requires local constraints AND a completed crossing check.
pub(all) struct Analysis {
  points : Array[CamPoint]
  pressure_sample_index : Int32
  offset_factor_sample_index : Int32
  max_pressure : Double
  pressure_angle : Double
  min_offset_factor : Double
  min_offset_factor_angle : Double
  max_velocity : Double
  max_acceleration : Double
  max_jerk : Double
  jerk_jumps : Int32
  sampled_local_feasible : Bool
  crossings : CrossingCheck
  crossing_witnesses : Array[CrossingWitness]
  crossing_witnesses_truncated : Bool
  sampled_feasible : Bool
  nominal_resolution : Int32
}

///|
priv struct MotionSample {
  theta : Double
  motion : Motion
}

///|
/// Geometry-independent mesh can be reused across every search candidate.
priv struct MotionMesh {
  samples : Array[MotionSample]
  endpoint_jerk : Double
  jerk_jumps : Int32
  nominal_resolution : Int32
}

///|
/// O(N+S) evaluation: the sampling loop already knows its segment index.
fn motion_mesh(p : Program, resolution : Int32) -> Result[MotionMesh, String] {
  if resolution < Resolution_min || resolution > Library_resolution_max {
    return Err(
      "resolution must be between \{Resolution_min} and \{Library_resolution_max}",
    )
  }
  let out : Array[MotionSample] = []
  let mut endpoint_jerk = 0.0
  for k = 0; k < p.segments.length(); k = k + 1 {
    let seg = p.segments[k]
    let n = @math.ceil(seg.angle / (2.0 * @math.PI) * resolution.to_double())
      .to_int()
      .max(1)
    for i = 0; i < n; i = i + 1 {
      let u = i.to_double() / n.to_double()
      let m = match segment_motion(seg, p.heights[k], u) {
        Ok(m) => m
        Err(e) => return Err(e)
      }
      out.push({ theta: p.starts[k] + seg.angle * u, motion: m, })
    }
    for u in [0.0, 1.0] {
      let m = match segment_motion(seg, p.heights[k], u) {
        Ok(m) => m
        Err(e) => return Err(e)
      }
      endpoint_jerk = endpoint_jerk.max(m.j.abs())
    }
  }
  out.push({ theta: 2.0 * @math.PI, motion: out[0].motion, })
  Ok({
    samples: out,
    endpoint_jerk,
    jerk_jumps: jerk_jumps(p),
    nominal_resolution: resolution,
  })
}

///|
fn jerk_jumps(p : Program) -> Int32 {
  let mut count = 0
  let n = p.segments.length()
  for i = 0; i < n; i = i + 1 {
    let l = p.segments[i]
    let r = p.segments[(i + 1) % n]
    let jl = l.lift *
      normalized(l.law, 1.0).unwrap().j /
      (l.angle * l.angle * l.angle)
    let jr = r.lift *
      normalized(r.law, 0.0).unwrap().j /
      (r.angle * r.angle * r.angle)
    if (jl - jr).abs() > 1.0e-8 * (1.0 + jl.abs() + jr.abs()) {
      count = count + 1
    }
  }
  count
}

///|
fn validate_analysis(pressure : Double, omega : Double) -> Result[Unit, String] {
  if !finite(pressure) || pressure <= 0.0 || pressure >= @math.PI / 2.0 {
    return Err("pressure limit must be in (0, pi/2) radians")
  }
  if !finite(omega) || omega <= 0.0 {
    return Err("speed must be finite and positive")
  }
  Ok(())
}

///|
priv struct Scan {
  metrics : Metrics
  points : Array[CamPoint]
}

///|
/// Preconditions: geometry, pressure and speed validated once at the boundary.
fn scan_mesh(
  mesh : MotionMesh,
  g : Geometry,
  pressure_limit : Double,
  omega : Double,
  retain : Bool,
) -> Result[Scan, String] {
  let q0 = zero_height(g)
  let points : Array[CamPoint] = []
  let mut pressure = 0.0
  let mut pressure_index = 0
  let mut factor_index = 0
  let mut pa = 0.0
  let mut factor = 0.0
  let mut first_factor = true
  let mut fa = 0.0
  let mut velocity = 0.0
  let mut acceleration = 0.0
  let mut jerk = mesh.endpoint_jerk * omega * omega * omega
  if !finite(jerk) {
    return Err("jerk overflow")
  }
  for index = 0; index < mesh.samples.length(); index = index + 1 {
    let item = mesh.samples[index]
    let geom = match local_geometry(g, q0, item.motion) {
      Ok(v) => v
      Err(e) => return Err(e)
    }
    if geom.pressure.abs() > pressure {
      pressure = geom.pressure.abs()
      pa = item.theta
      pressure_index = index
    }
    if first_factor || geom.factor < factor {
      first_factor = false
      factor = geom.factor
      fa = item.theta
      factor_index = index
    }
    let t = match scale_motion(item.motion, omega) {
      Ok(v) => v
      Err(e) => return Err(e)
    }
    velocity = velocity.max(t.v.abs())
    acceleration = acceleration.max(t.a.abs())
    jerk = jerk.max(t.j.abs())
    if retain {
      match contact_local(g, item.theta, item.motion, geom) {
        Ok(c) => points.push(c)
        Err(e) => return Err(e)
      }
    }
  }
  Ok({
    points,
    metrics: {
      pressure_sample_index: pressure_index,
      offset_factor_sample_index: factor_index,
      max_pressure: pressure,
      pressure_angle: pa,
      min_offset_factor: factor,
      min_offset_factor_angle: fa,
      max_velocity: velocity,
      max_acceleration: acceleration,
      max_jerk: jerk,
      jerk_jumps: mesh.jerk_jumps,
      sampled_local_feasible: pressure <= pressure_limit &&
      factor > Offset_factor_min,
      sample_count: mesh.samples.length(),
      nominal_resolution: mesh.nominal_resolution,
    },
  })
}

///|
/// Pure state combination: a failed or absent check can never accept a profile.
pub fn sampled_acceptance(local_ok : Bool, crossings : CrossingCheck) -> Bool {
  match crossings {
    Checked(0) => local_ok
    _ => false
  }
}

///|
fn finish_scan(scan : Scan) -> Analysis {
  let m = scan.metrics
  let witnesses : Array[CrossingWitness] = []
  let mut truncated = false
  let crossings = match crossing_report(scan.points) {
    Ok(r) => {
      for witness in r.witnesses {
        witnesses.push(witness)
      }
      truncated = r.witnesses_truncated
      if r.uncertain_pairs > 0 {
        Indeterminate(r.crossings, r.uncertain_pairs)
      } else {
        Checked(r.crossings)
      }
    }
    Err(e) => Failed(e)
  }
  {
    points: scan.points,
    pressure_sample_index: m.pressure_sample_index,
    offset_factor_sample_index: m.offset_factor_sample_index,
    max_pressure: m.max_pressure,
    pressure_angle: m.pressure_angle,
    min_offset_factor: m.min_offset_factor,
    min_offset_factor_angle: m.min_offset_factor_angle,
    max_velocity: m.max_velocity,
    max_acceleration: m.max_acceleration,
    max_jerk: m.max_jerk,
    jerk_jumps: m.jerk_jumps,
    sampled_local_feasible: m.sampled_local_feasible,
    crossings,
    crossing_witnesses: witnesses,
    crossing_witnesses_truncated: truncated,
    sampled_feasible: sampled_acceptance(m.sampled_local_feasible, crossings),
    nominal_resolution: m.nominal_resolution,
  }
}

///|
/// Full sampled analysis, including topology. Not a continuous geometry proof.
pub fn analyze(
  p : Program,
  g : Geometry,
  resolution : Int32,
  pressure_limit : Double,
  omega : Double,
) -> Result[Analysis, String] {
  match validate_analysis(pressure_limit, omega) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  match validate_geometry(g) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  let mesh = match motion_mesh(p, resolution) {
    Ok(m) => m
    Err(e) => return Err(e)
  }
  match scan_mesh(mesh, g, pressure_limit, omega, true) {
    Ok(s) => Ok(finish_scan(s))
    Err(e) => Err(e)
  }
}

///|
/// Metrics only: no CamPoint collection, trigonometry, topology or exports.
pub fn measure(
  p : Program,
  g : Geometry,
  resolution : Int32,
  pressure_limit : Double,
  omega : Double,
) -> Result[Metrics, String] {
  match validate_analysis(pressure_limit, omega) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  match validate_geometry(g) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  let mesh = match motion_mesh(p, resolution) {
    Ok(m) => m
    Err(e) => return Err(e)
  }
  match scan_mesh(mesh, g, pressure_limit, omega, false) {
    Ok(s) => Ok(s.metrics)
    Err(e) => Err(e)
  }
}

///|
/// The nominal resolution sets density; all segment boundaries are preserved.
pub fn sample(
  p : Program,
  g : Geometry,
  resolution : Int32,
) -> Result[Array[CamPoint], String] {
  match validate_geometry(g) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  let mesh = match motion_mesh(p, resolution) {
    Ok(m) => m
    Err(e) => return Err(e)
  }
  let q0 = zero_height(g)
  let out : Array[CamPoint] = []
  for item in mesh.samples {
    match contact_prepared(g, q0, item.theta, item.motion) {
      Ok(c) => out.push(c)
      Err(e) => return Err(e)
    }
  }
  Ok(out)
}