///|
pub struct GradientPixel {
  point : Point
  gx : Int
  gy : Int
  magnitude : Int
} derive(Debug, Eq)

///|
pub fn GradientPixel::new(
  point~ : Point,
  gx~ : Int,
  gy~ : Int,
) -> GradientPixel {
  { point, gx, gy, magnitude: abs_i(gx) + abs_i(gy) }
}

///|
pub struct GradientField {
  width : Int
  height : Int
  pixels : Array[GradientPixel]
} derive(Debug, Eq)

///|
pub fn GradientField::from_image(image : GrayImage) -> GradientField {
  let pixels : Array[GradientPixel] = []
  for y in 0.. Int {
  y * self.width + x
}

///|
pub fn GradientField::in_bounds(self : GradientField, x : Int, y : Int) -> Bool {
  x >= 0 && y >= 0 && x < self.width && y < self.height
}

///|
pub fn GradientField::get(
  self : GradientField,
  x : Int,
  y : Int,
) -> GradientPixel {
  if self.in_bounds(x, y) {
    self.pixels[self.index(x, y)]
  } else {
    GradientPixel::new(point=Point::new(x~, y~), gx=0, gy=0)
  }
}

///|
pub fn GradientField::magnitude_image(self : GradientField) -> GrayImage {
  let out = GrayImage::new(width=self.width, height=self.height)
  for pixel in self.pixels {
    out.set(pixel.point.x, pixel.point.y, Gray::new(pixel.magnitude))
  }
  out
}

///|
pub fn GradientField::direction_code(
  self : GradientField,
  x : Int,
  y : Int,
) -> Int {
  let pixel = self.get(x, y)
  if abs_i(pixel.gx) >= abs_i(pixel.gy) {
    if pixel.gx >= 0 {
      0
    } else {
      4
    }
  } else if pixel.gy >= 0 {
    2
  } else {
    6
  }
}

///|
pub fn GradientField::mean_magnitude(self : GradientField) -> Int {
  if self.pixels.length() == 0 {
    0
  } else {
    let mut total = 0
    for pixel in self.pixels {
      total += pixel.magnitude
    }
    total / self.pixels.length()
  }
}

///|
pub struct CornerFeature {
  point : Point
  score : Int
  response : Int
} derive(Debug, Eq)

///|
pub fn CornerFeature::new(
  point~ : Point,
  score~ : Int,
  response~ : Int,
) -> CornerFeature {
  { point, score, response }
}

///|
pub fn corner_response(
  field : GradientField,
  x : Int,
  y : Int,
  radius : Int,
) -> Int {
  let r = max_i(1, radius)
  let mut xx = 0
  let mut yy = 0
  let mut xy = 0
  for dy in -r..<=r {
    for dx in -r..<=r {
      let pixel = field.get(x + dx, y + dy)
      xx += pixel.gx * pixel.gx
      yy += pixel.gy * pixel.gy
      xy += pixel.gx * pixel.gy
    }
  }
  let determinant = xx * yy - xy * xy
  let trace = xx + yy
  determinant - trace * trace / 25
}

///|
pub fn detect_corners(
  image : GrayImage,
  threshold : Int,
  radius? : Int = 1,
  border? : Int = 1,
) -> Array[CornerFeature] {
  let field = GradientField::from_image(image)
  let out : Array[CornerFeature] = []
  let r = max_i(1, radius)
  let b = max_i(r, border)
  if image.width > b * 2 && image.height > b * 2 {
    for y in b..<(image.height - b) {
      for x in b..<(image.width - b) {
        let response = corner_response(field, x, y, r)
        if response > threshold && is_local_maximum(field, x, y, r, response) {
          out.push(
            CornerFeature::new(
              point=Point::new(x~, y~),
              score=response / max_i(1, r * r),
              response~,
            ),
          )
        }
      }
    }
  }
  out
}

///|
fn is_local_maximum(
  field : GradientField,
  x : Int,
  y : Int,
  radius : Int,
  response : Int,
) -> Bool {
  for dy in -radius..<=radius {
    for dx in -radius..<=radius {
      if dx != 0 || dy != 0 {
        if corner_response(field, x + dx, y + dy, 1) > response {
          return false
        }
      }
    }
  }
  true
}

///|
pub struct PatchDescriptor {
  center : Point
  radius : Int
  values : Array[Int]
} derive(Debug, Eq)

///|
pub fn PatchDescriptor::from_image(
  image : GrayImage,
  center : Point,
  radius : Int,
) -> PatchDescriptor {
  let r = max_i(0, radius)
  let values : Array[Int] = []
  let center_value = image.get(center.x, center.y).value
  for y in (center.y - r)..<=(center.y + r) {
    for x in (center.x - r)..<=(center.x + r) {
      values.push(image.get(x, y).value - center_value)
    }
  }
  { center, radius: r, values }
}

///|
pub fn PatchDescriptor::distance(
  self : PatchDescriptor,
  other : PatchDescriptor,
) -> Int {
  let count = min_i(self.values.length(), other.values.length())
  let mut total = 0
  for i in 0.. Array[PatchDescriptor] {
  let out : Array[PatchDescriptor] = []
  for point in points {
    out.push(PatchDescriptor::from_image(image, point, radius))
  }
  out
}

///|
pub fn projection_profile(
  image : GrayImage,
  horizontal : Bool,
  cutoff : Int,
) -> Array[Int] {
  let length = if horizontal { image.height } else { image.width }
  let out = Array::make(max_i(0, length), 0)
  for y in 0..= cutoff {
        let index = if horizontal { y } else { x }
        out[index] += 1
      }
    }
  }
  out
}

///|
pub fn find_runs(values : Array[Int], threshold : Int) -> Array[Rect] {
  let out : Array[Rect] = []
  let mut start = -1
  for i, value in values {
    if value >= threshold && start < 0 {
      start = i
    } else if value < threshold && start >= 0 {
      out.push(Rect::new(x=start, y=0, width=i - start, height=1))
      start = -1
    }
  }
  if start >= 0 {
    out.push(Rect::new(x=start, y=0, width=values.length() - start, height=1))
  }
  out
}

///|
pub fn detect_bright_regions(
  image : GrayImage,
  cutoff : Int,
  minimum_area : Int,
) -> Array[Box2D] {
  let binary = Mask::new(width=image.width, height=image.height)
  for y in 0..= cutoff {
        binary.set(x, y, 1)
      }
    }
  }
  let label = Label::new(id=1, name="bright-region")
  let out : Array[Box2D] = []
  for component in connected_components(binary, 1) {
    if component.area >= minimum_area {
      out.push(Box2D::new(label~, rect=component.bounds))
    }
  }
  out
}

///|
pub fn image_edge_density(image : GrayImage, threshold : Int) -> Int {
  let edges = image.edge_map(threshold)
  let mut count = 0
  for value in edges.pixels {
    if value > 0 {
      count += 1
    }
  }
  if edges.pixels.length() == 0 {
    0
  } else {
    count * 1000 / edges.pixels.length()
  }
}

///|
pub fn keypoints_to_annotations(
  points : Array[Point],
  label : Label,
  radius : Int,
) -> Array[InstanceAnnotation] {
  let out : Array[InstanceAnnotation] = []
  for i, point in points {
    out.push(
      InstanceAnnotation::new(
        id=i,
        label~,
        bbox=Box2D::new(
          label~,
          rect=Rect::new(
            x=point.x - radius,
            y=point.y - radius,
            width=radius * 2 + 1,
            height=radius * 2 + 1,
          ),
        ),
        keypoints=[Keypoint::new(label=label.name, point~, visible=true)],
      ),
    )
  }
  out
}