/// Frame CDEF preparation follows libaom cdef_find_dir_c, adjust_strength and
/// av1_cdef_filter_fb at revision 1a4b3aef9b3e5b9251c45e9f986f41894dd45bb2.
/// Source: https://aomedia.googlesource.com/aom/+/1a4b3aef9b3e5b9251c45e9f986f41894dd45bb2/av1/common/cdef_block.c
/// Copyright (c) 2016, Alliance for Open Media. All rights reserved.
/// BSD 2 Clause License and Alliance for Open Media Patent License 1.0:
/// https://www.aomedia.org/license/software and https://www.aomedia.org/license/patent

///|
/// Direction and directional variance for a validated complete luma 8x8 block.
/// Samples normalize to eight bits before the weighted line-cost search.
fn av1_cdef_find_direction(
  plane : Array[Int],
  stride : Int,
  x : Int,
  y : Int,
  bit_depth : Int,
) -> (Int, Int) {
  let partial : Array[Array[Int]] = []
  for _ in 0..<8 {
    partial.push(Array::make(15, 0))
  }
  let cost = Array::make(8, 0)
  let div_table = [0, 840, 420, 280, 210, 168, 140, 120, 105]
  let shift = bit_depth - 8
  for i in 0..<8 {
    for j in 0..<8 {
      let value = (plane[(y + i) * stride + x + j] >> shift) - 128
      partial[0][i + j] += value
      partial[1][i + j / 2] += value
      partial[2][i] += value
      partial[3][3 + i - j / 2] += value
      partial[4][7 + i - j] += value
      partial[5][3 - i / 2 + j] += value
      partial[6][j] += value
      partial[7][i / 2 + j] += value
    }
  }
  for i in 0..<8 {
    cost[2] += partial[2][i] * partial[2][i]
    cost[6] += partial[6][i] * partial[6][i]
  }
  cost[2] *= div_table[8]
  cost[6] *= div_table[8]
  for i in 0..<7 {
    cost[0] += (
        partial[0][i] * partial[0][i] + partial[0][14 - i] * partial[0][14 - i]
      ) *
      div_table[i + 1]
    cost[4] += (
        partial[4][i] * partial[4][i] + partial[4][14 - i] * partial[4][14 - i]
      ) *
      div_table[i + 1]
  }
  cost[0] += partial[0][7] * partial[0][7] * div_table[8]
  cost[4] += partial[4][7] * partial[4][7] * div_table[8]
  for direction in [1, 3, 5, 7] {
    for j in 0..<5 {
      cost[direction] += partial[direction][3 + j] * partial[direction][3 + j]
    }
    cost[direction] *= div_table[8]
    for j in 0..<3 {
      cost[direction] += (
          partial[direction][j] * partial[direction][j] +
          partial[direction][10 - j] * partial[direction][10 - j]
        ) *
        div_table[2 * j + 2]
    }
  }
  let mut best_direction = 0
  let mut best_cost = 0
  for direction in 0..<8 {
    if cost[direction] > best_cost {
      best_direction = direction
      best_cost = cost[direction]
    }
  }
  (best_direction, (best_cost - cost[(best_direction + 4) & 7]) >> 10)
}

///|
/// The input strength has already been scaled to native bit-depth units.
fn av1_cdef_adjust_strength(strength : Int, variance : Int) -> Int {
  if variance == 0 {
    return 0
  }
  let magnitude = variance >> 6
  let log = if magnitude == 0 { 0 } else { av1_cdef_floor_log2(magnitude) }
  let adjustment = if log < 12 { log } else { 12 }
  (strength * (4 + adjustment) + 8) >> 4
}

///|
/// Filter MI-padded monochrome or 4:2:0 planes before visible-frame cropping.
/// Skip flags use the luma 4x4 grid; indices use the frame's 64x64 grid.
fn av1_cdef_filter_frame(
  width : Int,
  height : Int,
  planes : Array[Array[Int]],
  skips : Array[Int],
  indices : Array[Int],
  config : Av1CdefConfig,
  bit_depth : Int,
  sub_x? : Int = 1,
  sub_y? : Int = 1,
) -> Array[Array[Int]]? {
  let monochrome = planes.length() == 1
  if width <= 0 ||
    height <= 0 ||
    width % 8 != 0 ||
    height % 8 != 0 ||
    width > MAX_IMAGE_PIXELS / height ||
    (!monochrome && planes.length() != 3) ||
    (bit_depth != 8 && bit_depth != 10 && bit_depth != 12) ||
    !av1_cdef_config_valid(config, monochrome~) {
    return None
  }
  let mi_cols = width / 4
  let mi_rows = height / 4
  let region_cols = (width + 63) / 64
  let region_rows = (height + 63) / 64
  if skips.length() != mi_cols * mi_rows ||
    indices.length() != region_cols * region_rows {
    return None
  }
  for skip in skips {
    if skip != 0 && skip != 1 {
      return None
    }
  }
  for index in indices {
    if index < -1 || index >= config.y_primary.length() {
      return None
    }
  }
  let maximum = (1 << bit_depth) - 1
  for plane_index in 0.. maximum {
        return None
      }
    }
  }
  let shift = bit_depth - 8
  let y_params : Array[Av1CdefNativeParams] = []
  let uv_params : Array[Av1CdefNativeParams] = []
  let disabled : Av1CdefNativeParams = {
    primary_strength: 0,
    secondary_strength: 0,
    damping: 0,
    direction: 0,
  }
  for by in 0..<(height / 8) {
    for bx in 0..<(width / 8) {
      let x = bx * 8
      let y = by * 8
      let index = indices[y / 64 * region_cols + x / 64]
      let mi = by * 2 * mi_cols + bx * 2
      let all_skip = skips[mi] == 1 &&
        skips[mi + 1] == 1 &&
        skips[mi + mi_cols] == 1 &&
        skips[mi + mi_cols + 1] == 1
      if index == -1 || all_skip {
        y_params.push(disabled)
        if !monochrome {
          uv_params.push(disabled)
        }
        continue
      }
      let (direction, variance) = av1_cdef_find_direction(
        planes[0],
        width,
        x,
        y,
        bit_depth,
      )
      let primary_y = config.y_primary[index] << shift
      y_params.push({
        primary_strength: av1_cdef_adjust_strength(primary_y, variance),
        secondary_strength: config.y_secondary[index] << shift,
        damping: config.damping + shift,
        direction: if primary_y == 0 {
          0
        } else {
          direction
        },
      })
      if !monochrome {
        let primary_uv = config.uv_primary[index] << shift
        uv_params.push({
          primary_strength: primary_uv,
          secondary_strength: config.uv_secondary[index] << shift,
          damping: config.damping + shift - 1,
          direction: if primary_uv == 0 {
            0
          } else if sub_x == 1 && sub_y == 0 {
            // CDEF maps luma directions to the horizontally subsampled
            // 4:2:2 chroma grid (dav1d cdef_apply_tmpl.c uv_dirs[1]).
            [7, 0, 2, 4, 5, 6, 6, 6][direction]
          } else {
            direction
          },
        })
      }
    }
  }
  let filtered_y = av1_cdef_filter_native_blocks(
    width,
    height,
    planes[0],
    y_params,
    bit_depth,
    8,
    8,
  )
  if monochrome {
    return Some([filtered_y])
  }
  let chroma_w = (width + (1 << sub_x) - 1) >> sub_x
  let chroma_h = (height + (1 << sub_y) - 1) >> sub_y
  let block_w = 8 >> sub_x
  let block_h = 8 >> sub_y
  Some([
    filtered_y,
    av1_cdef_filter_native_blocks(
      chroma_w,
      chroma_h,
      planes[1],
      uv_params,
      bit_depth,
      block_w,
      block_h,
    ),
    av1_cdef_filter_native_blocks(
      chroma_w,
      chroma_h,
      planes[2],
      uv_params,
      bit_depth,
      block_w,
      block_h,
    ),
  ])
}