///|
pub fn patch_values(image : GrayImage, bounds : Bounds) -> Array[Double] {
  let result : Array[Double] = []
  for y in bounds.min_y..<(bounds.max_y + 1) {
    for x in bounds.min_x..<(bounds.max_x + 1) {
      result.push(image.at(x, y))
    }
  }
  result
}

///|
pub fn normalized_patch_score(
  image : GrayImage,
  template : GrayImage,
  x : Int,
  y : Int,
) -> Double {
  if template.width <= 0 || template.height <= 0 {
    0.0
  } else {
    let image_values : Array[Double] = []
    let template_values : Array[Double] = []
    for dy in 0.. MatchResult {
  let mut found = false
  let mut location = Point::Point(x=0.0, y=0.0)
  let mut score = -2.0
  for y in 0..<(image.height - template.height + 1).max(0) {
    for x in 0..<(image.width - template.width + 1).max(0) {
      let current = normalized_patch_score(image, template, x, y)
      if current > score {
        score = current
        location = Point::Point(x=Double::from_int(x), y=Double::from_int(y))
      }
      if current >= minimum_score {
        found = true
      }
    }
  }
  { found, location, score, candidates: if found { [location] } else { [] } }
}

///|
pub fn template_candidates(
  image : GrayImage,
  template : GrayImage,
  minimum_score : Double,
  maximum_count : Int,
) -> Array[MatchResult] {
  let result : Array[MatchResult] = []
  for y in 0..<(image.height - template.height + 1).max(0) {
    for x in 0..<(image.width - template.width + 1).max(0) {
      let score = normalized_patch_score(image, template, x, y)
      if score >= minimum_score {
        let candidate = {
          found: true,
          location: Point::Point(x=Double::from_int(x), y=Double::from_int(y)),
          score,
          candidates: [],
        }
        let mut index = 0
        while index < result.length() && result[index].score >= score {
          index += 1
        }
        result.insert(index, candidate)
        if result.length() > maximum_count.max(1) {
          ignore(result.pop())
        }
      }
    }
  }
  result
}

///|
pub fn suppress_nearby_matches(
  matches : Array[MatchResult],
  radius : Double,
) -> Array[MatchResult] {
  let result : Array[MatchResult] = []
  for item in matches {
    let mut keep = true
    for selected in result {
      if distance(item.location, selected.location) < radius.max(0.0) {
        keep = false
      }
    }
    if keep {
      result.push(item)
    }
  }
  result
}

///|
pub fn image_patch(
  image : GrayImage,
  top_left : Point,
  width : Int,
  height : Int,
) -> GrayImage {
  let result = GrayImage(width=width.max(0), height=height.max(0))
  for y in 0.. GrayImage {
  let width = first.width.min(second.width)
  let height = first.height.min(second.height)
  let result = GrayImage(width~, height~)
  for y in 0.. Double {
  let difference = patch_difference(first, second)
  if difference.pixels.is_empty() {
    0.0
  } else {
    mean_value(difference.pixels.map(fn(value) { value * value }))
  }
}

///|
pub fn patch_peak_signal_to_noise(
  first : GrayImage,
  second : GrayImage,
) -> Double {
  let mse = patch_mean_squared_error(first, second)
  if mse <= 0.0000000001 {
    100.0
  } else {
    10.0 * @math.log10(1.0 / mse)
  }
}

///|
pub fn match_score_map(image : GrayImage, template : GrayImage) -> GrayImage {
  let width = (image.width - template.width + 1).max(0)
  let height = (image.height - template.height + 1).max(0)
  let result = GrayImage(width~, height~)
  for y in 0.. Histogram {
  histogram(match_score_map(image, template), bins, bounds={
    min: -1.0,
    max: 1.0,
  })
}

///|
pub fn match_centroid(matches : Array[MatchResult]) -> Point {
  if matches.is_empty() {
    Point::Point(x=0.0, y=0.0)
  } else {
    let mut x = 0.0
    let mut y = 0.0
    for item in matches {
      x += item.location.x
      y += item.location.y
    }
    Point::Point(
      x=x / Double::from_int(matches.length()),
      y=y / Double::from_int(matches.length()),
    )
  }
}

///|
pub fn match_confidence(
  match_result : MatchResult,
  minimum_score : Double,
) -> Double {
  normalized_quality(match_result.score, minimum_score, 1.0)
}

///|
pub fn match_result_report(match_result : MatchResult) -> String {
  let out = StringBuilder()
  out.write_string("found=")
  out.write_string(match_result.found.to_string())
  out.write_string("; score=")
  out.write_string(match_result.score.to_string())
  out.write_string("; location=(")
  out.write_string(match_result.location.x.to_string())
  out.write_string(",")
  out.write_string(match_result.location.y.to_string())
  out.write_string(")")
  out.to_string()
}

///|
pub fn compare_templates(
  image : GrayImage,
  first : GrayImage,
  second : GrayImage,
) -> Int {
  let first_score = best_template_match(image, first, -1.0).score
  let second_score = best_template_match(image, second, -1.0).score
  if first_score > second_score {
    1
  } else if first_score < second_score {
    -1
  } else {
    0
  }
}