/// CDEF pixel kernel for caller-supplied block directions and strengths.
///
/// Direction search, luma variance adjustment, skipped-block selection and
/// frame-header parsing are performed by the caller before this kernel.

///|
/// Effective CDEF parameters for one luma 8x8 block (chroma 4x4 in 4:2:0).
/// Strengths use 8-bit units: primary is already variance-adjusted and
/// secondary is decoded to 0, 1, 2 or 4. Damping is CdefDamping (3..6).
pub(all) struct Av1CdefParams {
  primary_strength : Int
  secondary_strength : Int
  damping : Int
  direction : Int
}

///|
/// Native sample units preserve fractional 8-bit-unit luma strengths after
/// variance adjustment. Damping includes bit-depth and chroma adjustments.
priv struct Av1CdefNativeParams {
  primary_strength : Int
  secondary_strength : Int
  damping : Int
  direction : Int
}

///|
fn av1_cdef_offset(direction : Int, distance : Int) -> (Int, Int) {
  match direction {
    0 => (distance, -distance)
    1 => (distance, -(distance / 2))
    2 => (distance, 0)
    3 => (distance, distance / 2)
    4 => (distance, distance)
    5 => (distance / 2, distance)
    6 => (0, distance)
    _ => (-(distance / 2), distance)
  }
}

///|
fn av1_cdef_floor_log2(value : Int) -> Int {
  let mut n = value
  let mut log = 0
  while n > 1 {
    n = n >> 1
    log += 1
  }
  log
}

///|
fn av1_cdef_constrain(diff : Int, threshold : Int, damping : Int) -> Int {
  if threshold == 0 {
    return 0
  }
  let magnitude = if diff < 0 { -diff } else { diff }
  let shift = damping - av1_cdef_floor_log2(threshold)
  let limit = threshold - (magnitude >> (if shift > 0 { shift } else { 0 }))
  let clipped = if limit < 0 {
    0
  } else if magnitude < limit {
    magnitude
  } else {
    limit
  }
  if diff < 0 {
    -clipped
  } else {
    clipped
  }
}

///|
/// Filter from the source snapshot. Taps beyond the frame are unavailable;
/// valid taps across block boundaries still read the unfiltered source.
fn av1_cdef_filter_blocks(
  width : Int,
  height : Int,
  plane : Array[Int],
  params : Array[Av1CdefParams],
  bit_depth : Int,
  block_size : Int,
  damping_offset : Int,
) -> Array[Int]? {
  if width <= 0 ||
    height <= 0 ||
    width > MAX_IMAGE_PIXELS / height ||
    plane.length() != width * height {
    return None
  }
  if bit_depth != 8 && bit_depth != 10 && bit_depth != 12 {
    return None
  }
  let blocks_x = (width + block_size - 1) / block_size
  let blocks_y = (height + block_size - 1) / block_size
  if params.length() != blocks_x * blocks_y {
    return None
  }
  for p in params {
    if p.primary_strength < 0 ||
      p.primary_strength > 15 ||
      !(p.secondary_strength == 0 ||
      p.secondary_strength == 1 ||
      p.secondary_strength == 2 ||
      p.secondary_strength == 4) ||
      p.damping < 3 ||
      p.damping > 6 ||
      p.direction < 0 ||
      p.direction > 7 {
      return None
    }
  }
  let max_value = (1 << bit_depth) - 1
  for value in plane {
    if value < 0 || value > max_value {
      return None
    }
  }
  let shift = bit_depth - 8
  let native : Array[Av1CdefNativeParams] = []
  for p in params {
    native.push({
      primary_strength: p.primary_strength << shift,
      secondary_strength: p.secondary_strength << shift,
      damping: p.damping + shift + damping_offset,
      direction: p.direction,
    })
  }
  Some(
    av1_cdef_filter_native_blocks(
      width, height, plane, native, bit_depth, block_size, block_size,
    ),
  )
}

///|
/// Internal kernel; callers validate geometry, samples and parameter counts.
/// All taps read the immutable source plane, including across 64x64 regions.
/// A CDEF block is 8x8 luma pixels, so a subsampled chroma block is
/// (8 >> sub_x) by (8 >> sub_y) chroma pixels and still maps one-to-one onto
/// the luma 8x8 parameter grid.
fn av1_cdef_filter_native_blocks(
  width : Int,
  height : Int,
  plane : Array[Int],
  params : Array[Av1CdefNativeParams],
  bit_depth : Int,
  block_w : Int,
  block_h : Int,
) -> Array[Int] {
  let blocks_x = (width + block_w - 1) / block_w
  let out = plane.copy()
  let shift = bit_depth - 8
  for y in 0..> shift) & 1) != 0 {
              3
            } else if distance == 1 {
              4
            } else {
              2
            }
          } else if distance == 1 {
            2
          } else {
            1
          }
          for sign_index in 0..<2 {
            let sign = if sign_index == 0 { -1 } else { 1 }
            let sx = x + sign * dx
            let sy = y + sign * dy
            if sx < 0 || sx >= width || sy < 0 || sy >= height {
              continue
            }
            let sample = plane[sy * width + sx]
            if sample < minimum {
              minimum = sample
            }
            if sample > maximum {
              maximum = sample
            }
            sum += weight *
              av1_cdef_constrain(sample - center, strength, damping)
          }
        }
      }
      let filtered = center + ((8 + sum - (if sum < 0 { 1 } else { 0 })) >> 4)
      out[y * width + x] = if filtered < minimum {
        minimum
      } else if filtered > maximum {
        maximum
      } else {
        filtered
      }
    }
  }
  out
}

///|
/// Apply CDEF to a plane using one parameter record per 8x8 block.
/// Frame-edge taps are excluded and the input plane remains unchanged.
pub fn av1_cdef_filter_plane(
  width : Int,
  height : Int,
  plane : Array[Int],
  params : Array[Av1CdefParams],
  bit_depth? : Int = 8,
) -> Array[Int]? {
  av1_cdef_filter_blocks(width, height, plane, params, bit_depth, 8, 0)
}

///|
/// Filter 4:2:0 planes. Both parameter arrays follow the luma 8x8 grid;
/// each UV record controls the corresponding chroma 4x4 block.
/// Chroma uses the normative damping decrement of one.
pub fn av1_cdef_filter_yuv420(
  width : Int,
  height : Int,
  y : Array[Int],
  u : Array[Int],
  v : Array[Int],
  y_params : Array[Av1CdefParams],
  uv_params : Array[Av1CdefParams],
  bit_depth? : Int = 8,
) -> (Array[Int], Array[Int], Array[Int])? {
  if width <= 0 || height <= 0 || width > MAX_IMAGE_PIXELS / height {
    return None
  }
  let cw = (width + 1) / 2
  let ch = (height + 1) / 2
  let yf = match
    av1_cdef_filter_blocks(width, height, y, y_params, bit_depth, 8, 0) {
    Some(value) => value
    None => return None
  }
  let uf = match
    av1_cdef_filter_blocks(cw, ch, u, uv_params, bit_depth, 4, -1) {
    Some(value) => value
    None => return None
  }
  let vf = match
    av1_cdef_filter_blocks(cw, ch, v, uv_params, bit_depth, 4, -1) {
    Some(value) => value
    None => return None
  }
  Some((yf, uf, vf))
}