// Additional byte-safe filters for texture inspection and robust feature input.

///|
pub(all) struct Kernel1D {
  radius : Int
  weights : Array[Double]
  normalizer : Double
}

///|
pub fn Kernel1D::box(radius : Int) -> Kernel1D {
  let safe_radius = if radius < 0 { 0 } else { radius }
  let size = safe_radius * 2 + 1
  {
    radius: safe_radius,
    weights: Array::make(size, 1.0),
    normalizer: Double::from_int(size),
  }
}

///|
pub fn Kernel1D::triangle(radius : Int) -> Kernel1D {
  let safe_radius = if radius < 0 { 0 } else { radius }
  let weights = Array::make(safe_radius * 2 + 1, 0.0)
  let mut total = 0.0
  let mut offset = -safe_radius
  while offset <= safe_radius {
    let weight = Double::from_int(
      safe_radius + 1 - (if offset < 0 { -offset } else { offset }),
    )
    weights[offset + safe_radius] = weight
    total = total + weight
    offset = offset + 1
  }
  { radius: safe_radius, weights, normalizer: total }
}

///|
pub fn Kernel1D::gaussian_like(radius : Int, sigma : Double) -> Kernel1D {
  let safe_radius = if radius < 0 { 0 } else { radius }
  let safe_sigma = if sigma <= 0.0 { 1.0 } else { sigma }
  let weights = Array::make(safe_radius * 2 + 1, 0.0)
  let mut total = 0.0
  let mut offset = -safe_radius
  while offset <= safe_radius {
    let distance = Double::from_int(if offset < 0 { -offset } else { offset })
    let scaled = distance / safe_sigma
    let weight = 1.0 / (1.0 + scaled * scaled)
    weights[offset + safe_radius] = weight
    total = total + weight
    offset = offset + 1
  }
  { radius: safe_radius, weights, normalizer: total }
}

///|
pub fn Kernel1D::normalized(self : Kernel1D) -> Kernel1D {
  if self.normalizer <= 0.0 {
    return self
  }
  let weights = Array::make(self.weights.length(), 0.0)
  let mut i = 0
  while i < self.weights.length() {
    weights[i] = self.weights[i] / self.normalizer
    i = i + 1
  }
  { radius: self.radius, weights, normalizer: 1.0 }
}

///|
fn filter_clamp(value : Double) -> Int {
  let rounded = (value + 0.5).to_int()
  if rounded < 0 {
    0
  } else if rounded > 255 {
    255
  } else {
    rounded
  }
}

///|
pub fn apply_kernel_horizontal(img : GrayImage, kernel : Kernel1D) -> GrayImage {
  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 value = 0.0
      let mut offset = -kernel.radius
      while offset <= kernel.radius {
        let weight = kernel.weights[offset + kernel.radius]
        value = value +
          weight *
          Double::from_int(img.sample(x + offset, y, BorderMode::Clamp))
        offset = offset + 1
      }
      let normalized = if kernel.normalizer == 1.0 {
        value
      } else {
        value / kernel.normalizer
      }
      result.set(x, y, filter_clamp(normalized))
      x = x + 1
    }
    y = y + 1
  }
  result
}

///|
pub fn apply_kernel_vertical(img : GrayImage, kernel : Kernel1D) -> GrayImage {
  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 value = 0.0
      let mut offset = -kernel.radius
      while offset <= kernel.radius {
        let weight = kernel.weights[offset + kernel.radius]
        value = value +
          weight *
          Double::from_int(img.sample(x, y + offset, BorderMode::Clamp))
        offset = offset + 1
      }
      let normalized = if kernel.normalizer == 1.0 {
        value
      } else {
        value / kernel.normalizer
      }
      result.set(x, y, filter_clamp(normalized))
      x = x + 1
    }
    y = y + 1
  }
  result
}

///|
pub fn apply_separable_kernel(img : GrayImage, kernel : Kernel1D) -> GrayImage {
  apply_kernel_vertical(apply_kernel_horizontal(img, kernel), kernel)
}

///|
pub fn smooth_triangle(img : GrayImage, radius : Int) -> GrayImage {
  apply_separable_kernel(img, Kernel1D::triangle(radius))
}

///|
pub fn smooth_gaussian_like(
  img : GrayImage,
  radius : Int,
  sigma : Double,
) -> GrayImage {
  apply_separable_kernel(img, Kernel1D::gaussian_like(radius, sigma))
}

///|
fn filter_sort(values : Array[Int]) -> Unit {
  let mut i = 1
  while i < values.length() {
    let value = values[i]
    let mut j = i
    while j > 0 && value < values[j - 1] {
      values[j] = values[j - 1]
      j = j - 1
    }
    values[j] = value
    i = i + 1
  }
}

///|
pub fn rank_filter(img : GrayImage, radius : Int, rank : Int) -> GrayImage {
  let result = GrayImage::new(img.width, img.height)
  let safe_radius = if radius < 0 { 0 } else { radius }
  let side = safe_radius * 2 + 1
  let count = side * side
  if count <= 0 {
    return result
  }
  let mut y = 0
  while y < img.height {
    let mut x = 0
    while x < img.width {
      let values = Array::make(count, 0)
      let mut cursor = 0
      let mut dy = -safe_radius
      while dy <= safe_radius {
        let mut dx = -safe_radius
        while dx <= safe_radius {
          values[cursor] = img.sample(x + dx, y + dy, BorderMode::Clamp)
          cursor = cursor + 1
          dx = dx + 1
        }
        dy = dy + 1
      }
      filter_sort(values)
      let slot = if rank < 0 {
        0
      } else if rank >= count {
        count - 1
      } else {
        rank
      }
      result.set(x, y, values[slot])
      x = x + 1
    }
    y = y + 1
  }
  result
}

///|
pub fn median_filter(img : GrayImage, radius : Int) -> GrayImage {
  let safe_radius = if radius < 0 { 0 } else { radius }
  let side = safe_radius * 2 + 1
  rank_filter(img, safe_radius, side * side / 2)
}

///|
pub fn percentile_filter(
  img : GrayImage,
  radius : Int,
  percentile : Double,
) -> GrayImage {
  let safe_radius = if radius < 0 { 0 } else { radius }
  let side = safe_radius * 2 + 1
  let count = side * side
  let mut probability = percentile
  if probability < 0.0 {
    probability = 0.0
  }
  if probability > 1.0 {
    probability = 1.0
  }
  rank_filter(
    img,
    safe_radius,
    (probability * Double::from_int(count - 1)).to_int(),
  )
}

///|
pub fn laplacian_response(img : GrayImage) -> GrayImage {
  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 center = img.get(x, y) * 4
      let neighbors = img.sample(x - 1, y, BorderMode::Clamp) +
        img.sample(x + 1, y, BorderMode::Clamp) +
        img.sample(x, y - 1, BorderMode::Clamp) +
        img.sample(x, y + 1, BorderMode::Clamp)
      let response = center - neighbors
      result.set(x, y, if response < 0 { -response } else { response })
      x = x + 1
    }
    y = y + 1
  }
  result
}

///|
pub fn unsharp_mask(
  img : GrayImage,
  radius : Int,
  amount : Double,
) -> GrayImage {
  let blurred = smooth_gaussian_like(
    img,
    if radius < 0 {
      0
    } else {
      radius
    },
    1.0,
  )
  let result = GrayImage::new(img.width, img.height)
  let safe_amount = if amount < 0.0 { 0.0 } else { amount }
  let mut i = 0
  while i < img.data.length() {
    let detail = Double::from_int(img.data[i] - blurred.data[i])
    result.data[i] = filter_clamp(
      Double::from_int(img.data[i]) + detail * safe_amount,
    )
    i = i + 1
  }
  result
}

///|
pub(all) struct ThresholdSummary {
  threshold : Int
  foreground : Int
  background : Int
  mean_foreground : Double
  mean_background : Double
}

///|
pub fn otsu_summary(img : GrayImage) -> ThresholdSummary {
  let histogram = img.histogram()
  let total = img.data.length()
  if total == 0 {
    return {
      threshold: 0,
      foreground: 0,
      background: 0,
      mean_foreground: 0.0,
      mean_background: 0.0,
    }
  }
  let mut weighted_total = 0
  let mut i = 0
  while i < 256 {
    weighted_total = weighted_total + histogram[i] * i
    i = i + 1
  }
  let mut weight_background = 0
  let mut sum_background = 0
  let mut best_threshold = 0
  let mut best_between = -1.0
  i = 0
  while i < 256 {
    weight_background = weight_background + histogram[i]
    sum_background = sum_background + histogram[i] * i
    let weight_foreground = total - weight_background
    if weight_background > 0 && weight_foreground > 0 {
      let mean_background = Double::from_int(sum_background) /
        Double::from_int(weight_background)
      let mean_foreground = Double::from_int(weighted_total - sum_background) /
        Double::from_int(weight_foreground)
      let difference = mean_background - mean_foreground
      let between = Double::from_int(weight_background * weight_foreground) *
        difference *
        difference
      if between > best_between {
        best_between = between
        best_threshold = i
      }
    }
    i = i + 1
  }
  let threshold_image = binary_threshold(img, best_threshold)
  let foreground_count = threshold_image.data.length() -
    count_zero_pixels(threshold_image)
  let background_count = threshold_image.data.length() - foreground_count
  let foreground_sum = sum_selected_pixels(img, threshold_image, true)
  let background_sum = sum_selected_pixels(img, threshold_image, false)
  {
    threshold: best_threshold,
    foreground: foreground_count,
    background: background_count,
    mean_foreground: if foreground_count == 0 {
      0.0
    } else {
      foreground_sum / Double::from_int(foreground_count)
    },
    mean_background: if background_count == 0 {
      0.0
    } else {
      background_sum / Double::from_int(background_count)
    },
  }
}

///|
fn count_zero_pixels(img : GrayImage) -> Int {
  let mut count = 0
  let mut i = 0
  while i < img.data.length() {
    if img.data[i] == 0 {
      count = count + 1
    }
    i = i + 1
  }
  count
}

///|
fn sum_selected_pixels(
  img : GrayImage,
  mask : GrayImage,
  foreground : Bool,
) -> Double {
  let mut total = 0.0
  let mut i = 0
  while i < img.data.length() && i < mask.data.length() {
    let selected = if foreground { mask.data[i] > 0 } else { mask.data[i] == 0 }
    if selected {
      total = total + Double::from_int(img.data[i])
    }
    i = i + 1
  }
  total
}

///|
pub fn otsu_threshold(img : GrayImage) -> Int {
  otsu_summary(img).threshold
}

///|
pub fn threshold_otsu(img : GrayImage) -> GrayImage {
  binary_threshold(img, otsu_threshold(img))
}

///|
pub fn threshold_sauvola(
  img : GrayImage,
  radius : Int,
  sensitivity : Double,
  bias : Double,
) -> GrayImage {
  let result = GrayImage::new(img.width, img.height)
  let integral = IntegralImage::build(img)
  let mut y = 0
  while y < img.height {
    let mut x = 0
    while x < img.width {
      let window = integral.window(x, y, radius)
      let mean = integral.mean(window)
      let variance = integral.variance(window)
      let deviation = if variance <= 0.0 {
        0.0
      } else {
        let mut estimate = if variance > 1.0 { variance } else { 1.0 }
        let mut i = 0
        while i < 16 {
          estimate = (estimate + variance / estimate) / 2.0
          i = i + 1
        }
        estimate
      }
      let threshold = mean * (1.0 + sensitivity * (deviation / 128.0 - 1.0)) +
        bias
      if Double::from_int(img.get(x, y)) >= threshold {
        result.set(x, y, 255)
      }
      x = x + 1
    }
    y = y + 1
  }
  result
}

///|
pub fn contrast_stretch(img : GrayImage, low : Int, high : Int) -> GrayImage {
  let result = GrayImage::new(img.width, img.height)
  let mut minimum = low
  let mut maximum = high
  if minimum < 0 {
    minimum = 0
  }
  if maximum > 255 {
    maximum = 255
  }
  if maximum <= minimum {
    return result
  }
  let span = maximum - minimum
  let mut i = 0
  while i < img.data.length() {
    let value = img.data[i]
    let mapped = if value <= minimum {
      0
    } else if value >= maximum {
      255
    } else {
      (value - minimum) * 255 / span
    }
    result.data[i] = mapped
    i = i + 1
  }
  result
}

///|
pub fn quantize_image(img : GrayImage, levels : Int) -> GrayImage {
  let result = GrayImage::new(img.width, img.height)
  if levels <= 1 {
    return result
  }
  let steps = levels - 1
  let mut i = 0
  while i < img.data.length() {
    let quantized = (img.data[i] * steps + 127) / 255
    result.data[i] = quantized * 255 / steps
    i = i + 1
  }
  result
}

///|
pub fn image_add(left : GrayImage, right : GrayImage) -> GrayImage {
  let width = if left.width < right.width { left.width } else { right.width }
  let height = if left.height < right.height {
    left.height
  } else {
    right.height
  }
  let result = GrayImage::new(width, height)
  let mut y = 0
  while y < height {
    let mut x = 0
    while x < width {
      result.set(x, y, left.get(x, y) + right.get(x, y))
      x = x + 1
    }
    y = y + 1
  }
  result
}

///|
pub fn image_subtract(left : GrayImage, right : GrayImage) -> GrayImage {
  let width = if left.width < right.width { left.width } else { right.width }
  let height = if left.height < right.height {
    left.height
  } else {
    right.height
  }
  let result = GrayImage::new(width, height)
  let mut y = 0
  while y < height {
    let mut x = 0
    while x < width {
      result.set(x, y, left.get(x, y) - right.get(x, y))
      x = x + 1
    }
    y = y + 1
  }
  result
}

///|
pub fn image_blend(
  left : GrayImage,
  right : GrayImage,
  weight : Double,
) -> GrayImage {
  let width = if left.width < right.width { left.width } else { right.width }
  let height = if left.height < right.height {
    left.height
  } else {
    right.height
  }
  let result = GrayImage::new(width, height)
  let safe_weight = if weight < 0.0 {
    0.0
  } else if weight > 1.0 {
    1.0
  } else {
    weight
  }
  let mut y = 0
  while y < height {
    let mut x = 0
    while x < width {
      let value = Double::from_int(left.get(x, y)) * (1.0 - safe_weight) +
        Double::from_int(right.get(x, y)) * safe_weight
      result.set(x, y, filter_clamp(value))
      x = x + 1
    }
    y = y + 1
  }
  result
}

///|
pub fn local_binary_pattern(img : GrayImage, radius : Int) -> GrayImage {
  let result = GrayImage::new(img.width, img.height)
  let safe_radius = if radius <= 0 { 1 } else { radius }
  let offsets : Array[(Int, Int)] = [
    (0, -1),
    (1, -1),
    (1, 0),
    (1, 1),
    (0, 1),
    (-1, 1),
    (-1, 0),
    (-1, -1),
  ]
  let mut y = 0
  while y < img.height {
    let mut x = 0
    while x < img.width {
      let center = img.get(x, y)
      let mut code = 0
      let mut i = 0
      while i < offsets.length() {
        let (dx, dy) = offsets[i]
        if img.sample(
            x + dx * safe_radius,
            y + dy * safe_radius,
            BorderMode::Clamp,
          ) >=
          center {
          code = code | (1 << i)
        }
        i = i + 1
      }
      result.set(x, y, code)
      x = x + 1
    }
    y = y + 1
  }
  result
}

///|
pub fn texture_histogram(img : GrayImage) -> Array[Int] {
  let histogram = Array::make(256, 0)
  let mut i = 0
  while i < img.data.length() {
    histogram[img.data[i]] = histogram[img.data[i]] + 1
    i = i + 1
  }
  histogram
}

///|
pub fn block_texture_histogram(img : GrayImage, rect : Rect) -> Array[Int] {
  let clipped = img.crop(rect)
  texture_histogram(local_binary_pattern(clipped, 1))
}

///|
pub fn edge_strength(img : GrayImage) -> GrayImage {
  let gradients = sobel_gradients(img)
  gradient_magnitude(gradients)
}

///|
pub fn edge_threshold(img : GrayImage, threshold : Int) -> GrayImage {
  binary_threshold(edge_strength(img), threshold)
}

///|
pub fn edge_density(img : GrayImage, threshold : Int) -> Double {
  if img.data.length() == 0 {
    return 0.0
  }
  let edges = edge_threshold(img, threshold)
  let mut count = 0
  let mut i = 0
  while i < edges.data.length() {
    if edges.data[i] > 0 {
      count = count + 1
    }
    i = i + 1
  }
  Double::from_int(count) / Double::from_int(edges.data.length())
}

///|
pub fn gradient_energy(img : GrayImage, rect : Rect) -> Double {
  let field = sobel_gradients(img)
  let (left, top, right, bottom) = (
    if rect.x0 < 0 {
      0
    } else if rect.x0 > img.width {
      img.width
    } else {
      rect.x0
    },
    if rect.y0 < 0 {
      0
    } else if rect.y0 > img.height {
      img.height
    } else {
      rect.y0
    },
    if rect.x1 > img.width {
      img.width
    } else if rect.x1 < 0 {
      0
    } else {
      rect.x1
    },
    if rect.y1 > img.height {
      img.height
    } else if rect.y1 < 0 {
      0
    } else {
      rect.y1
    },
  )
  let mut total = 0.0
  let mut count = 0
  let mut y = top
  while y < bottom {
    let mut x = left
    while x < right {
      let index = y * img.width + x
      total = total +
        field.gx[index] * field.gx[index] +
        field.gy[index] * field.gy[index]
      count = count + 1
      x = x + 1
    }
    y = y + 1
  }
  if count == 0 {
    0.0
  } else {
    total / Double::from_int(count)
  }
}

///|
pub fn non_maximum_response(response : GrayImage, radius : Int) -> GrayImage {
  let result = GrayImage::new(response.width, response.height)
  let safe_radius = if radius < 0 { 0 } else { radius }
  let mut y = 0
  while y < response.height {
    let mut x = 0
    while x < response.width {
      let value = response.get(x, y)
      let mut maximum = true
      let mut dy = -safe_radius
      while dy <= safe_radius {
        let mut dx = -safe_radius
        while dx <= safe_radius {
          if (dx != 0 || dy != 0) && response.get(x + dx, y + dy) > value {
            maximum = false
          }
          dx = dx + 1
        }
        dy = dy + 1
      }
      if maximum {
        result.set(x, y, value)
      }
      x = x + 1
    }
    y = y + 1
  }
  result
}