// Grayscale preprocessing and gradient analysis.

///|
pub(all) struct GradientField {
  width : Int
  height : Int
  gx : Array[Double]
  gy : Array[Double]
}

///|
fn abs_double(value : Double) -> Double {
  if value < 0.0 {
    -value
  } else {
    value
  }
}

///|
pub fn box_blur(img : GrayImage, radius : Int) -> GrayImage {
  if radius <= 0 {
    return img.clone()
  }
  let result = GrayImage::new(img.width, img.height)
  let diameter = radius * 2 + 1
  let area = diameter * diameter
  let mut y = 0
  while y < img.height {
    let mut x = 0
    while x < img.width {
      let mut sum = 0
      let mut dy = -radius
      while dy <= radius {
        let mut dx = -radius
        while dx <= radius {
          sum = sum + img.sample(x + dx, y + dy, BorderMode::Clamp)
          dx = dx + 1
        }
        dy = dy + 1
      }
      result.set(x, y, (sum + area / 2) / area)
      x = x + 1
    }
    y = y + 1
  }
  result
}

///|
pub fn gaussian_blur(img : GrayImage, radius : Int) -> GrayImage {
  if radius <= 0 {
    return img.clone()
  }
  box_blur(box_blur(img, radius), radius)
}

///|
pub fn sobel_gradients(img : GrayImage) -> GradientField {
  let gx = Array::make(img.width * img.height, 0.0)
  let gy = Array::make(img.width * img.height, 0.0)
  let mut y = 0
  while y < img.height {
    let mut x = 0
    while x < img.width {
      let left = img.sample(x - 1, y, BorderMode::Clamp)
      let right = img.sample(x + 1, y, BorderMode::Clamp)
      let top = img.sample(x, y - 1, BorderMode::Clamp)
      let bottom = img.sample(x, y + 1, BorderMode::Clamp)
      let index = y * img.width + x
      gx[index] = Double::from_int(right - left) / 2.0
      gy[index] = Double::from_int(bottom - top) / 2.0
      x = x + 1
    }
    y = y + 1
  }
  { width: img.width, height: img.height, gx, gy }
}

///|
pub fn gradient_magnitude(field : GradientField) -> GrayImage {
  let result = GrayImage::new(field.width, field.height)
  let mut i = 0
  while i < field.gx.length() {
    let magnitude = (abs_double(field.gx[i]) + abs_double(field.gy[i])) / 2.0
    result.data[i] = if magnitude > 255.0 { 255 } else { magnitude.to_int() }
    i = i + 1
  }
  result
}

///|
fn approximate_direction(y : Double, x : Double) -> Double {
  let pi = 3.141592653589793
  if x == 0.0 && y == 0.0 {
    0.0
  } else if abs_double(x) >= abs_double(y) {
    let ratio = y / x
    let mut angle = ratio - ratio * ratio * ratio / 3.0
    if x < 0.0 {
      angle = if y >= 0.0 { angle + pi } else { angle - pi }
    }
    angle
  } else {
    let ratio = x / y
    let mut angle = ratio - ratio * ratio * ratio / 3.0
    if y >= 0.0 {
      angle = 1.5707963267948966 - angle
    } else {
      angle = -1.5707963267948966 - angle
    }
    angle
  }
}

///|
pub fn gradient_orientation(field : GradientField) -> Array[Double] {
  let result = Array::make(field.gx.length(), 0.0)
  let mut i = 0
  while i < result.length() {
    result[i] = approximate_direction(field.gy[i], field.gx[i])
    i = i + 1
  }
  result
}

///|
pub fn local_contrast(img : GrayImage, radius : Int) -> GrayImage {
  if radius <= 0 {
    return GrayImage::new(img.width, img.height)
  }
  let result = GrayImage::new(img.width, img.height)
  let mut y = 0
  while y < img.height {
    let mut x = 0
    while x < img.width {
      let mut low = 255
      let mut high = 0
      let mut dy = -radius
      while dy <= radius {
        let mut dx = -radius
        while dx <= radius {
          let value = img.sample(x + dx, y + dy, BorderMode::Clamp)
          if value < low {
            low = value
          }
          if value > high {
            high = value
          }
          dx = dx + 1
        }
        dy = dy + 1
      }
      result.set(x, y, high - low)
      x = x + 1
    }
    y = y + 1
  }
  result
}