///|
/// Local Binary Pattern (LBP) descriptors.
///
/// The implementation uses the classic 8-neighbour, radius-one operator.
/// Bits are assigned clockwise starting at the top-left neighbour.  Border
/// pixels do not have a complete neighbourhood: their code is zero and they
/// are omitted from the histogram.  Keeping this convention explicit makes
/// histograms comparable across images of different sizes without inventing
/// out-of-bounds sample values.

///| Internal BT.601 luma helper used by the descriptor hot loop.
fn lbp_luma(image : Image, x : Int, y : Int) -> Int {
  let i = (y * image.width + x) * 4
  (
    299 * image.data[i].to_int() +
    587 * image.data[i + 1].to_int() +
    114 * image.data[i + 2].to_int()
  ) / 1000
}

///| Returns one 8-bit LBP code for every pixel in row-major order.
///
/// For an interior pixel, bit `0` is top-left and subsequent bits proceed
/// clockwise (`top`, `top-right`, ..., `left`).  A bit is set when its
/// neighbour's luma is greater than or equal to the centre luma.  Border
/// entries are zero because they cannot provide eight samples.
pub fn Image::lbp_codes(self : Image) -> FixedArray[Byte] {
  let n = self.pixel_count()
  let codes = FixedArray::make(n, b'\x00')
  if self.width < 3 || self.height < 3 {
    return codes
  }
  let offsets : FixedArray[(Int, Int)] = [
    (-1, -1),
    (0, -1),
    (1, -1),
    (1, 0),
    (1, 1),
    (0, 1),
    (-1, 1),
    (-1, 0),
  ]
  for y in 1..<(self.height - 1) {
    for x in 1..<(self.width - 1) {
      let centre = lbp_luma(self, x, y)
      let mut code = 0
      for bit in 0..<8 {
        let (dx, dy) = offsets[bit]
        if lbp_luma(self, x + dx, y + dy) >= centre {
          code = code | (1 << bit)
        }
      }
      codes[y * self.width + x] = code.to_byte()
    }
  }
  codes
}

///| Returns the 256-bin histogram of radius-one LBP codes.
///
/// Only pixels with a complete 3x3 neighbourhood contribute, so the bin sum
/// is `max(width - 2, 0) * max(height - 2, 0)`.  The histogram is useful as a
/// compact texture descriptor and is invariant to the image's absolute
/// brightness when local ordering is preserved.
pub fn Image::lbp_histogram(self : Image) -> FixedArray[Int] {
  let histogram = FixedArray::make(256, 0)
  let codes = self.lbp_codes()
  if self.width < 3 || self.height < 3 {
    return histogram
  }
  for y in 1..<(self.height - 1) {
    for x in 1..<(self.width - 1) {
      let code = codes[y * self.width + x].to_int()
      histogram[code] = histogram[code] + 1
    }
  }
  histogram
}