///|
priv struct Parameters {
  width : Double
  height : Double
  hole_radius : Double
  horizontal_margin : Double
  vertical_margin : Double
}

///|
/// All geometry and relation handles survive edits on this one sketch.
priv struct Bracket {
  sketch : @sketch.Sketch
  corners : Array[@sketch.PointId]
  axis_points : Array[@sketch.PointId]
  centers : Array[@sketch.PointId]
  holes : Array[@sketch.CircleId]
  width : @sketch.ConstraintId
  height : @sketch.ConstraintId
  radius : @sketch.ConstraintId
  horizontal_margin : @sketch.ConstraintId
  vertical_margin : @sketch.ConstraintId
  conflict : @sketch.ConstraintId
}

///|
fn require_true(value : Bool, message : String) -> Unit {
  if !value {
    abort(message)
  }
}

///|
fn near(value : Double, expected : Double) -> Unit {
  if value.is_nan() || value.is_inf() || (value - expected).abs() > 1.0e-5 {
    abort("Bracket geometry \{value}; expected \{expected}")
  }
}

///|
/// The demo accepts separated holes inside the plate. This is an application
/// input check, not an inequality constraint implemented by the core solver.
fn validate_parameters(p : Parameters) -> Unit {
  for
    value in [
      p.width,
      p.height,
      p.hole_radius,
      p.horizontal_margin,
      p.vertical_margin,
    ] {
    require_true(
      !value.is_nan() && !value.is_inf() && value > 0.0,
      "Bracket parameters must be finite and positive",
    )
  }
  require_true(
    p.horizontal_margin > p.hole_radius &&
    p.vertical_margin > p.hole_radius &&
    p.width > 2.0 * (p.horizontal_margin + p.hole_radius) &&
    p.height > 2.0 * (p.vertical_margin + p.hole_radius),
    "Demo holes must stay inside the plate and separated",
  )
}

///|
fn new_bracket(p : Parameters) -> Bracket {
  validate_parameters(p)
  let s = @sketch.Sketch::new()
  // Deliberately approximate seeds, not coordinates computed from dimensions.
  // Corner order: bottom-left, bottom-right, top-right, top-left.
  let corners = [
    s.add_point(0.0, 0.0),
    s.add_point(118.0, 1.0),
    s.add_point(121.0, 79.0),
    s.add_point(-1.0, 81.0),
  ]
  let bottom = s.add_line(corners[0], corners[1])
  let right = s.add_line(corners[1], corners[2])
  let top = s.add_line(corners[2], corners[3])
  // Downward left edge: its positive signed distance points into the plate.
  let left = s.add_line(corners[3], corners[0])
  s.constrain(Fix(corners[0], 0.0, 0.0)) |> ignore
  s.constrain(Horizontal(bottom)) |> ignore
  s.constrain(Vertical(right)) |> ignore
  s.constrain(Horizontal(top)) |> ignore
  s.constrain(Vertical(left)) |> ignore
  let width = s.constrain(DistanceX(corners[0], corners[1], p.width))
  let height = s.constrain(DistanceY(corners[0], corners[3], p.height))
  let axis_points = [
    s.add_point(60.0, 0.0),
    s.add_point(60.0, 80.0),
    s.add_point(0.0, 40.0),
    s.add_point(120.0, 40.0),
  ]
  s.constrain(Midpoint(axis_points[0], bottom)) |> ignore
  s.constrain(Midpoint(axis_points[1], top)) |> ignore
  s.constrain(Midpoint(axis_points[2], left)) |> ignore
  s.constrain(Midpoint(axis_points[3], right)) |> ignore
  let vertical_axis = s.add_line(axis_points[0], axis_points[1])
  let horizontal_axis = s.add_line(axis_points[2], axis_points[3])
  // Hole order: bottom-left, bottom-right, top-left, top-right.
  let centers = [
    s.add_point(18.0, 16.0),
    s.add_point(103.0, 14.0),
    s.add_point(19.0, 63.0),
    s.add_point(101.0, 64.0),
  ]
  let horizontal_margin = s.constrain(
    DistanceToLine(centers[0], left, p.horizontal_margin),
  )
  let vertical_margin = s.constrain(
    DistanceToLine(centers[0], bottom, p.vertical_margin),
  )
  s.constrain(Mirror(centers[0], centers[1], vertical_axis)) |> ignore
  s.constrain(Mirror(centers[0], centers[2], horizontal_axis)) |> ignore
  s.constrain(Mirror(centers[1], centers[3], horizontal_axis)) |> ignore
  let holes = centers.map(center => s.add_circle(center, 4.5))
  let radius = s.constrain(Radius(holes[0], p.hole_radius))
  for i = 1; i < holes.length(); i = i + 1 {
    s.constrain(EqualRadius(holes[0], holes[i])) |> ignore
  }
  let conflict = s.constrain(DistanceX(corners[0], corners[1], p.width + 10.0))
  s.set_constraint_enabled(conflict, false) |> ignore
  {
    sketch: s,
    corners,
    axis_points,
    centers,
    holes,
    width,
    height,
    radius,
    horizontal_margin,
    vertical_margin,
    conflict,
  }
}

///|
/// Editing only replaces driving relations; it never assigns solved coordinates.
fn Bracket::edit(self : Bracket, p : Parameters) -> Unit {
  validate_parameters(p)
  let s = self.sketch
  require_true(
    s.set_constraint(
      self.width,
      DistanceX(self.corners[0], self.corners[1], p.width),
    ) &&
    s.set_constraint(
      self.height,
      DistanceY(self.corners[0], self.corners[3], p.height),
    ) &&
    s.set_constraint(self.radius, Radius(self.holes[0], p.hole_radius)),
    "Missing driving dimension",
  )
  // Find the stored left and bottom lines through the existing relations.
  let left = match s.constraint(self.horizontal_margin) {
    Some(DistanceToLine(_, line, _)) => line
    _ => abort("Missing horizontal margin relation")
  }
  let bottom = match s.constraint(self.vertical_margin) {
    Some(DistanceToLine(_, line, _)) => line
    _ => abort("Missing vertical margin relation")
  }
  require_true(
    s.set_constraint(
      self.horizontal_margin,
      DistanceToLine(self.centers[0], left, p.horizontal_margin),
    ) &&
    s.set_constraint(
      self.vertical_margin,
      DistanceToLine(self.centers[0], bottom, p.vertical_margin),
    ),
    "Missing margin dimension",
  )
}

///|
/// Independent closed-form oracle, used only to verify the solver's result.
fn Bracket::verify(self : Bracket, p : Parameters) -> Unit {
  let expected_corners = [
    (0.0, 0.0),
    (p.width, 0.0),
    (p.width, p.height),
    (0.0, p.height),
  ]
  let expected_axes = [
    (p.width / 2.0, 0.0),
    (p.width / 2.0, p.height),
    (0.0, p.height / 2.0),
    (p.width, p.height / 2.0),
  ]
  let expected_centers = [
    (p.horizontal_margin, p.vertical_margin),
    (p.width - p.horizontal_margin, p.vertical_margin),
    (p.horizontal_margin, p.height - p.vertical_margin),
    (p.width - p.horizontal_margin, p.height - p.vertical_margin),
  ]
  for i, id in self.corners {
    let point = self.sketch.point(id).unwrap()
    near(point.x, expected_corners[i].0)
    near(point.y, expected_corners[i].1)
  }
  for i, id in self.axis_points {
    let point = self.sketch.point(id).unwrap()
    near(point.x, expected_axes[i].0)
    near(point.y, expected_axes[i].1)
  }
  for i, id in self.centers {
    let point = self.sketch.point(id).unwrap()
    near(point.x, expected_centers[i].0)
    near(point.y, expected_centers[i].1)
    near(self.sketch.radius(self.holes[i]).unwrap(), p.hole_radius)
  }
}

///|
fn Bracket::solve_checked(self : Bracket, p : Parameters) -> @sketch.Solution {
  let solution = match self.sketch.solve_and_apply() {
    Ok(solution) => solution
    Err(error) => abort(error.message())
  }
  require_true(
    solution.report.is_satisfied() &&
    solution.report.local_dof == 0 &&
    solution.report.variables == 28 &&
    solution.report.equations == 28 &&
    solution.report.dependent_equations == 0,
    "Bracket must be satisfied and locally fully constrained",
  )
  self.verify(p)
  solution
}

///|
fn Bracket::geometry_values(self : Bracket) -> Array[Double] {
  let values = []
  for ids in [self.corners, self.axis_points, self.centers] {
    for id in ids {
      let point = self.sketch.point(id).unwrap()
      values.push(point.x)
      values.push(point.y)
    }
  }
  for id in self.holes {
    values.push(self.sketch.radius(id).unwrap())
  }
  values
}

///|
fn Bracket::reject_conflict(self : Bracket, p : Parameters) -> @sketch.Solution {
  let saved = self.geometry_values()
  require_true(
    self.sketch.set_constraint(
      self.conflict,
      DistanceX(self.corners[0], self.corners[1], p.width + 10.0),
    ) &&
    self.sketch.set_constraint_enabled(self.conflict, true),
    "Missing conflict relation",
  )
  let failed = match self.sketch.solve_and_apply() {
    Ok(solution) => solution
    Err(error) => abort(error.message())
  }
  require_true(
    !failed.report.is_satisfied() && failed.report.unsatisfied().length() > 0,
    "Contradictory width must not be applied",
  )
  let retained = self.geometry_values()
  for i, value in saved {
    require_true(value == retained[i], "Failed solve changed bracket geometry")
  }
  for finding in failed.report.unsatisfied() {
    require_true(
      self.sketch.constraint(finding.id) is Some(_),
      "Diagnostic no longer maps to a relation",
    )
  }
  failed
}

///|
fn points_json(s : @sketch.Sketch, ids : Array[@sketch.PointId]) -> String {
  let mut text = "["
  for i, id in ids {
    if i > 0 {
      text = text + ","
    }
    let point = s.point(id).unwrap()
    text = text + "[\{point.x},\{point.y}]"
  }
  text + "]"
}

///|
fn Bracket::emit(
  self : Bracket,
  stage : String,
  p : Parameters,
  solution : @sketch.Solution,
) -> Unit {
  let s = self.sketch
  let report = solution.report
  let status = match report.status {
    Satisfied => "Satisfied"
    Stalled => "Stalled"
    IterationLimit => "IterationLimit"
  }
  let mut holes = "["
  for i, id in self.holes {
    if i > 0 {
      holes = holes + ","
    }
    let point = s.point(self.centers[i]).unwrap()
    let radius = s.radius(id).unwrap()
    holes = holes + "[\{point.x},\{point.y},\{radius}]"
  }
  holes = holes + "]"
  let mut residuals = "["
  for i, residual in report.residuals {
    if i > 0 {
      residuals = residuals + ","
    }
    residuals = residuals +
      "{\"id\":\{residual.id.index()},\"active\":\{residual.active},\"length_error\":\{residual.length_error},\"angular_error\":\{residual.angular_error},\"tolerance_ratio\":\{residual.tolerance_ratio}}"
  }
  residuals = residuals + "]"
  let title = match stage {
    "initial" =>
      "BEFORE: \{p.width} x \{p.height} | R\{p.hole_radius} | horizontal margin \{p.horizontal_margin}"
    "edited" =>
      "AFTER: \{p.width} x \{p.height} | R\{p.hole_radius} | horizontal margin \{p.horizontal_margin}"
    "conflict" =>
      "CONFLICT: widths \{p.width} and \{p.width + 10.0} | stored geometry retained"
    _ => "CAD sketch: " + stage
  }
  let svg = s
    .to_svg(options={
      ..@sketch.SvgOptions::default(),
      title,
      bounds: Some({ min_x: -12.0, min_y: -12.0, max_x: 180.0, max_y: 100.0, }),
    })
    .unwrap()
  let corners = points_json(s, self.corners)
  let axes = points_json(s, self.axis_points)
  println(
    "{\"stage\":\"\{stage}\",\"parameters\":{\"width\":\{p.width},\"height\":\{p.height},\"hole_radius\":\{p.hole_radius},\"horizontal_margin\":\{p.horizontal_margin},\"vertical_margin\":\{p.vertical_margin}},\"constraint_ids\":{\"width\":\{self.width.index()},\"height\":\{self.height.index()},\"radius\":\{self.radius.index()},\"horizontal_margin\":\{self.horizontal_margin.index()},\"vertical_margin\":\{self.vertical_margin.index()},\"conflict\":\{self.conflict.index()}},\"geometry\":{\"corners\":\{corners},\"axis_points\":\{axes},\"holes\":\{holes}},\"report\":{\"status\":\"\{status}\",\"local_dof\":\{report.local_dof},\"variables\":\{report.variables},\"equations\":\{report.equations},\"iterations\":\{report.iterations},\"max_tolerance_ratio\":\{report.max_tolerance_ratio},\"residuals\":\{residuals}},\"applied\":\{report.is_satisfied()},\"svg\":\{svg.escape()}}",
  )
}

///|
/// JSON Lines are read by scripts/bracket_showcase.cjs for independent checking
/// and SVG generation. A failed assertion aborts with a nonzero exit status.
fn main {
  let initial : Parameters = {
    width: 120.0,
    height: 80.0,
    hole_radius: 5.0,
    horizontal_margin: 20.0,
    vertical_margin: 15.0,
  }
  let bracket = new_bracket(initial)
  bracket.emit("initial", initial, bracket.solve_checked(initial))
  let widened : Parameters = { ..initial, width: 160.0, }
  bracket.edit(widened)
  bracket.emit("widened", widened, bracket.solve_checked(widened))
  let larger_holes : Parameters = { ..widened, hole_radius: 8.0, }
  bracket.edit(larger_holes)
  bracket.emit(
    "larger_holes",
    larger_holes,
    bracket.solve_checked(larger_holes),
  )
  let edited : Parameters = { ..larger_holes, horizontal_margin: 30.0, }
  bracket.edit(edited)
  bracket.emit("edited", edited, bracket.solve_checked(edited))
  bracket.emit("conflict", edited, bracket.reject_conflict(edited))
  require_true(
    bracket.sketch.set_constraint_enabled(bracket.conflict, false),
    "Cannot disable conflict",
  )
  bracket.emit("recovered", edited, bracket.solve_checked(edited))
}
