///|
/// Configuration for rendering a 3D synthetic marker scene.
pub(all) struct SyntheticSceneConfig {
  image_size : Size2D
  camera : CameraIntrinsics
  marker_id : Int
  dictionary : MarkerDictionary
  marker_length : Double
  pose : Pose3D
  noise_std : Double
  lighting_ambient : Double
  lighting_diffuse : Double
} derive(Debug, Eq)

///|
/// Creates default synthetic scene configuration.
pub fn SyntheticSceneConfig::default(
  marker_id : Int,
  dictionary : MarkerDictionary,
) -> SyntheticSceneConfig {
  let size = Size2D::new(640, 480)
  let cam = CameraIntrinsics::new(500.0, 500.0, 320.0, 240.0)
  let pose = Pose3D::new(
    Point3D::new(0.0, 0.0, 0.0),
    Point3D::new(0.0, 0.0, 0.5),
  )
  {
    image_size: size,
    camera: cam,
    marker_id,
    dictionary,
    marker_length: 0.1,
    pose,
    noise_std: 0.0,
    lighting_ambient: 0.8,
    lighting_diffuse: 0.2,
  }
}

///|
/// Renders a full 3D synthetic marker scene onto a camera image plane.
pub fn render_synthetic_scene(config : SyntheticSceneConfig) -> Image {
  let w = config.image_size.width
  let h = config.image_size.height
  let canvas = Image::new(w, h)
  canvas.fill(200) // Light gray background

  // Generate marker texture
  let marker_tex_size = 200
  let marker_tex = match
    generate_synthetic_marker_image(
      config.marker_id,
      config.dictionary,
      marker_tex_size,
      0,
    ) {
    Some(img) => img
    None => return canvas
  }

  // 3D Corner coordinates of marker on board plane (Z = 0)
  let half_s = config.marker_length * 0.5
  // Camera/image coordinates use Y-down, so the image top edge is -Y.
  let c_tl = Point3D::new(-half_s, -half_s, 0.0)
  let c_tr = Point3D::new(half_s, -half_s, 0.0)
  let c_br = Point3D::new(half_s, half_s, 0.0)
  let c_bl = Point3D::new(-half_s, half_s, 0.0)

  // Project 3D corners onto 2D image plane
  let p_tl = project_3d_point(c_tl, config.pose, config.camera)
  let p_tr = project_3d_point(c_tr, config.pose, config.camera)
  let p_br = project_3d_point(c_br, config.pose, config.camera)
  let p_bl = project_3d_point(c_bl, config.pose, config.camera)

  let quad = Quad::new(p_tl, p_tr, p_br, p_bl)
  let dst_corners = [
    Point2D::new(0.0, 0.0),
    Point2D::new(marker_tex_size.to_double(), 0.0),
    Point2D::new(marker_tex_size.to_double(), marker_tex_size.to_double()),
    Point2D::new(0.0, marker_tex_size.to_double()),
  ]

  // Homography mapping canvas quad -> marker texture.
  // find_homography returns the source-to-destination matrix, which is
  // exactly the canvas-to-texture mapping required by this rasterizer.
  match find_homography(quad.corners, dst_corners) {
    Some(canvas_to_texture) => {
      let bbox = quad.bounding_rect()
      let min_x = if bbox.x < 0.0 { 0 } else { bbox.x.floor().to_int() }
      let min_y = if bbox.y < 0.0 { 0 } else { bbox.y.floor().to_int() }
      let max_x = if bbox.x + bbox.width >= w.to_double() {
        w - 1
      } else {
        (bbox.x + bbox.width).ceil().to_int()
      }
      let max_y = if bbox.y + bbox.height >= h.to_double() {
        h - 1
      } else {
        (bbox.y + bbox.height).ceil().to_int()
      }

      let poly = Polygon2D::new(quad.corners)

      for y in min_y..<=max_y {
        for x in min_x..<=max_x {
          let pt = Point2D::new(x.to_double() + 0.5, y.to_double() + 0.5)
          if poly.contains_point(pt) {
            let tex_pt = canvas_to_texture.transform_point2d(pt)
            if tex_pt.x >= 0.0 &&
              tex_pt.x < marker_tex_size.to_double() &&
              tex_pt.y >= 0.0 &&
              tex_pt.y < marker_tex_size.to_double() {
              let px = marker_tex.sample_bilinear(tex_pt)
              let lit_px = (px * config.lighting_ambient).round().to_int()
              canvas.set(x, y, lit_px)
            }
          }
        }
      }
    }
    None => ()
  }

  // Optional Gaussian noise
  if config.noise_std > 0.0 {
    let mut rng_state = 12345L
    let next_gaussian = fn() -> Double {
      rng_state = rng_state * 6364136223846793005L + 1442695040888963407L
      let u1 = ((rng_state & 0x7fffffffL).to_double() + 1.0) / 2147483648.0
      rng_state = rng_state * 6364136223846793005L + 1442695040888963407L
      let u2 = ((rng_state & 0x7fffffffL).to_double() + 1.0) / 2147483648.0
      (-2.0 * @math.ln(u1)).sqrt() * @math.cos(2.0 * 3.141592653589793 * u2)
    }

    for y in 0..