///|
/// Checks if two quads represent the same marker by comparing corner distances.
pub fn are_quads_duplicate(
  q1 : Quad,
  q2 : Quad,
  min_corner_dist : Double,
) -> Bool {
  if q1.corners.length() != 4 || q2.corners.length() != 4 {
    return false
  }
  let mut sum_dist = 0.0
  for i in 0..<4 {
    sum_dist += q1.corners[i].distance(q2.corners[i])
  }
  sum_dist / 4.0 < min_corner_dist
}

///|
/// Comprehensive Marker Detection Pipeline parameterized with DetectorParameters.
pub fn detect_markers_with_params(
  img : Image,
  dict : MarkerDictionary,
  params : DetectorParameters,
  camera_matrix? : CameraIntrinsics?,
  marker_length? : Double = 0.05,
) -> Array[MarkerDetection] {
  let w = img.width
  let h = img.height
  if w < 10 || h < 10 {
    return []
  }

  let total_area = (w * h).to_double()
  let min_perim = (w + h).to_double() * params.min_marker_perimeter_rate
  let max_perim = (w + h).to_double() * params.max_marker_perimeter_rate
  let min_area = total_area * 0.0002

  let all_quad_candidates : Array[Quad] = []

  // Multi-scale adaptive threshold loop
  let mut win_size = params.adaptive_thresh_win_size_min
  while win_size <= params.adaptive_thresh_win_size_max {
    let binarized = threshold_adaptive(
      img,
      win_size,
      params.adaptive_thresh_constant,
    )
    let contours = find_contours(binarized, 15)

    for c in contours {
      let perim = c.perimeter()
      let area = c.area()
      if perim >= min_perim && perim <= max_perim && area >= min_area {
        let epsilon = perim * params.polygonal_approx_accuracy_rate
        let approx = rdp_simplify(c.points, epsilon)

        if approx.length() == 4 || approx.length() == 5 {
          let four_pts = if approx.length() == 5 {
            [approx[0], approx[1], approx[2], approx[3]]
          } else {
            approx
          }
          let quad = order_quad_corners(four_pts)
          if is_quad_convex(quad) {
            // Check distance to border
            let bbox = quad.bounding_rect()
            let min_b = params.min_distance_to_border.to_double()
            if bbox.x >= min_b &&
              bbox.y >= min_b &&
              bbox.x + bbox.width <= w.to_double() - min_b &&
              bbox.y + bbox.height <= h.to_double() - min_b {
              all_quad_candidates.push(quad)
            }
          }
        }
      }
    }

    win_size += params.adaptive_thresh_win_size_step
  }

  // Remove duplicate quad candidates
  let unique_quads : Array[Quad] = []
  for q in all_quad_candidates {
    let mut is_dup = false
    let min_dist = q.perimeter() * params.min_corner_distance_rate
    for u in unique_quads {
      if are_quads_duplicate(q, u, min_dist) {
        is_dup = true
        break
      }
    }
    if !is_dup {
      unique_quads.push(q)
    }
  }

  // Decode each unique quad candidate
  let detections : Array[MarkerDetection] = []
  let detected_ids : Array[Int] = []

  for q in unique_quads {
    // Optional corner refinement
    let refined_quad = if params.corner_refinement_method == 1 {
      refine_quad_corners(
        img,
        q,
        window_radius=params.corner_refinement_win_size,
      )
    } else {
      q
    }

    match decode_quad_marker(img, refined_quad, dict) {
      Some(det) => {
        // Check if ID already detected with lower Hamming distance
        let mut replace_idx = -1
        for i in 0.. estimate_marker_pose(det.quad, cam, marker_length)
          _ => None
        }

        let final_det = {
          id: det.id,
          quad: det.quad,
          pose,
          rotation_index: det.rotation_index,
          hamming_distance: det.hamming_distance,
        }

        if replace_idx >= 0 {
          detections[replace_idx] = final_det
        } else if replace_idx == -1 {
          detections.push(final_det)
          detected_ids.push(det.id)
        }
      }
      None => ()
    }
  }

  detections
}