///|
priv struct Transform {
  x : Double
  y : Double
  angle : Double
  scale : Double
} derive(ToJson)

///|
priv struct Record {
  group : String
  index : Int32
  family : String
  seed : Int32
  parameters : Array[Double]
  transform : Transform
  length_tolerance : Double
  angular_tolerance : Double
  initial_points : Array[Array[Double]]
  initial_radii : Array[Double]
  points : Array[Array[Double]]
  radii : Array[Double]
  stored_points : Array[Array[Double]]
  stored_radii : Array[Double]
  status : String
  error : String
  iterations : Int32
  variables : Int32
  equations : Int32
  local_dof : Int32
  max_tolerance_ratio : Double
  residual_ratios : Array[Double]
} derive(ToJson)

///|
// Park-Miller with Schrage's method: all intermediate values fit Int32.
// No platform RNG or time-based seed; each case can be regenerated in isolation.
fn random(state : Array[Int32]) -> Double {
  let value = 16807 * (state[0] % 127773) - 2836 * (state[0] / 127773)
  state[0] = if value > 0 { value } else { value + 2147483647 }
  state[0].to_double() / 2147483647.0
}

///|
fn transform(t : Transform, x : Double, y : Double) -> @sketch.Point {
  let c = @math.cos(t.angle)
  let s = @math.sin(t.angle)
  { x: t.x + (x * c - y * s) * t.scale, y: t.y + (x * s + y * c) * t.scale, }
}

///|
fn stored_points(
  s : @sketch.Sketch,
  ids : Array[@sketch.PointId],
) -> Array[Array[Double]] {
  ids.map(id => {
    let p = s.point(id).unwrap()
    [p.x, p.y]
  })
}

///|
fn run_case(group : Int32, index : Int32) -> Record {
  let seed = 20261022 + index * 37
  let state = [seed]
  // Mix nearby per-case seeds before drawing dimensions.
  for i = 0; i < 5; i = i + 1 {
    random(state) |> ignore
  }
  let w = 6.0 + 18.0 * random(state)
  let h = 4.0 + 10.0 * random(state)
  let r = 0.7 + 1.3 * random(state)
  let fraction = 0.25 + 0.5 * random(state)
  let family = index % 4
  let names = [
    "analytic", "translation", "rotation", "scaling", "perturbation", "extreme",
  ]
  let t : Transform = {
    x: if group == 1 {
      (index - 30).to_double() * 5000.0
    } else {
      0.0
    },
    y: if group == 1 {
      (index % 11 - 5).to_double() * 7000.0
    } else {
      0.0
    },
    angle: if group == 2 {
      -@math.PI + 2.0 * @math.PI * (index.to_double() + 0.5) / 60.0
    } else {
      0.0
    },
    scale: if group == 3 {
      @math.pow(10.0, -6.0 + 12.0 * index.to_double() / 59.0)
    } else if group == 5 {
      [
        1.0e-150, 1.0e-100, 1.0e-50, 1.0e-12, 1.0e-9, 1.0e9, 1.0e12, 1.0e50, 1.0e80,
        1.0e90,
      ][index / 4 % 10]
    } else {
      1.0
    },
  }
  let template = match family {
    0 => [[0.0, 0.0], [w, 0.0], [w * fraction, h]]
    1 => [[0.0, 0.0], [w, 0.0], [w, h], [0.0, h]]
    2 => [[-w, 0.0], [w, 0.0], [w * fraction, h], [w * fraction, -h]]
    _ => [[-w, 0.0], [w, 0.0], [0.0, r]]
  }
  let noise = if group == 4 { 0.25 * r } else { 0.04 * r }
  let perturb_state = [seed + (if group == 4 { 100000 } else { 0 })]
  let s = @sketch.Sketch::new()
  let ids = template.map(p => {
    let q = transform(
      t,
      p[0] + noise * (2.0 * random(perturb_state) - 1.0),
      p[1] + noise * (2.0 * random(perturb_state) - 1.0),
    )
    s.add_point(q.x, q.y)
  })
  let target = template.map(p => transform(t, p[0], p[1]))
  for i = 0; i < 2; i = i + 1 {
    s.constrain(Fix(ids[i], target[i].x, target[i].y)) |> ignore
  }
  let circles = []
  match family {
    0 => {
      s.constrain(
        Distance(ids[0], ids[2], @math.hypot(w * fraction, h) * t.scale),
      )
      |> ignore
      s.constrain(
        Distance(ids[1], ids[2], @math.hypot(w * (1.0 - fraction), h) * t.scale),
      )
      |> ignore
    }
    1 => {
      let ab = s.add_line(ids[0], ids[1])
      let bc = s.add_line(ids[1], ids[2])
      let cd = s.add_line(ids[2], ids[3])
      let ad = s.add_line(ids[0], ids[3])
      s.constrain(Perpendicular(ab, bc)) |> ignore
      s.constrain(Distance(ids[1], ids[2], h * t.scale)) |> ignore
      s.constrain(Parallel(ab, cd)) |> ignore
      s.constrain(Distance(ids[2], ids[3], w * t.scale)) |> ignore
      s.constrain(Parallel(bc, ad)) |> ignore
    }
    2 => {
      let axis = s.add_line(ids[0], ids[1])
      let a = s.add_circle(
        ids[2],
        r * t.scale * (1.0 + 0.1 * random(perturb_state)),
      )
      let b = s.add_circle(
        ids[3],
        r * t.scale * (1.0 + 0.1 * random(perturb_state)),
      )
      circles.push(a)
      circles.push(b)
      s.constrain(Fix(ids[2], target[2].x, target[2].y)) |> ignore
      s.constrain(Mirror(ids[2], ids[3], axis)) |> ignore
      s.constrain(Radius(a, r * t.scale)) |> ignore
      s.constrain(EqualRadius(a, b)) |> ignore
    }
    _ => {
      let axis = s.add_line(ids[0], ids[1])
      let circle = s.add_circle(
        ids[2],
        r * t.scale * (1.0 + 0.1 * random(perturb_state)),
      )
      circles.push(circle)
      s.constrain(Radius(circle, r * t.scale)) |> ignore
      s.constrain(Tangent(axis, circle)) |> ignore
      s.constrain(Distance(ids[0], ids[2], @math.hypot(w, r) * t.scale))
      |> ignore
    }
  }
  let initial_points = stored_points(s, ids)
  let initial_radii = circles.map(id => s.radius(id).unwrap())
  let length_tolerance = 1.0e-7 * t.scale
  let angular_tolerance = 1.0e-8
  let record : Record = {
    group: names[group],
    index,
    family: ["triangle", "rectangle", "mirror_circles", "line_circle_tangent"][family],
    seed,
    parameters: [w, h, r, fraction],
    transform: t,
    length_tolerance,
    angular_tolerance,
    initial_points,
    initial_radii,
    points: [],
    radii: [],
    stored_points: [],
    stored_radii: [],
    status: "Error",
    error: "",
    iterations: 0,
    variables: 0,
    equations: 0,
    local_dof: 0,
    max_tolerance_ratio: 0.0,
    residual_ratios: [],
  }
  match
    s.solve_and_apply(options={
      ..@sketch.SolveOptions::default(),
      length_tolerance,
      angular_tolerance,
    }) {
    Err(error) =>
      {
        ..record,
        error: error.message(),
        stored_points: stored_points(s, ids),
        stored_radii: circles.map(id => s.radius(id).unwrap()),
      }
    Ok(result) => {
      let report = result.report
      {
        ..record,
        points: ids.map(id => {
          let p = result.point(id).unwrap()
          [p.x, p.y]
        }),
        radii: circles.map(id => result.radius(id).unwrap()),
        stored_points: stored_points(s, ids),
        stored_radii: circles.map(id => s.radius(id).unwrap()),
        status: match report.status {
          Satisfied => "Satisfied"
          Stalled => "Stalled"
          IterationLimit => "IterationLimit"
        },
        iterations: report.iterations,
        variables: report.variables,
        equations: report.equations,
        local_dof: report.local_dof,
        max_tolerance_ratio: report.max_tolerance_ratio,
        residual_ratios: report.residuals.map(row => row.tolerance_ratio),
      }
    }
  }
}

///|
fn main {
  println(
    "{\"kind\":\"validation_suite\",\"schema\":1,\"seed\":20261022,\"cases\":400}",
  )
  for group = 0; group < 6; group = group + 1 {
    let count = if group == 4 { 100 } else { 60 }
    for index = 0; index < count; index = index + 1 {
      println(@json.to_json(run_case(group, index)).stringify())
    }
  }
}