/// Film-grain synthesis for reconstructed AV1 frames (AV1 spec section 7.18.3).
///
/// Synthesis runs on the visible output picture after the loop filters and
/// superres upscaling and before the colour conversion. Reference frames keep
/// grain-free samples, so the caller's planes are copied and never mutated.
///
/// The implementation mirrors the go-av1 reference (decode/filmgrain.go), whose
/// AR coefficient counts are confirmed by byte alignment on encoder output:
/// `numPosLuma = 2 * lag * (lag + 1)` for the luma coefficients plus one extra
/// chroma coefficient (the centre tap, used for the luma-average term) whenever
/// `num_y_points > 0`. See _refs/film_grain_sim.py.

///|
pub(all) struct Av1FilmGrainParams {
  apply_grain : Bool
  grain_seed : Int
  num_y_points : Int
  point_y_value : Array[Int]
  point_y_scaling : Array[Int]
  chroma_scaling_from_luma : Bool
  num_cb_points : Int
  point_cb_value : Array[Int]
  point_cb_scaling : Array[Int]
  num_cr_points : Int
  point_cr_value : Array[Int]
  point_cr_scaling : Array[Int]
  grain_scaling_minus8 : Int
  ar_coeff_lag : Int
  /// AR coefficients exactly as coded; the signed value is `entry - 128`.
  ar_coeffs_y : Array[Int]
  ar_coeffs_cb : Array[Int]
  ar_coeffs_cr : Array[Int]
  ar_coeff_shift_minus6 : Int
  grain_scale_shift : Int
  cb_mult : Int
  cb_luma_mult : Int
  cb_offset : Int
  cr_mult : Int
  cr_luma_mult : Int
  cr_offset : Int
  overlap_flag : Bool
  clip_to_restricted_range : Bool
}

///|
/// `(x + (1 << (n - 1))) >> n`, the AV1 round2() helper. Shifting by zero is
/// the identity, which 12-bit streams reach through `grain_scale_shift`.
fn av1_grain_round2(value : Int, shift : Int) -> Int {
  if shift <= 0 {
    value
  } else {
    (value + (1 << (shift - 1))) >> shift
  }
}

///|
fn av1_grain_clip3(low : Int, high : Int, value : Int) -> Int {
  if value < low {
    low
  } else if value > high {
    high
  } else {
    value
  }
}

///|
/// Read an AR coefficient as a signed value, treating an unwritten entry as the
/// zero-valued `0 - 128` the reference's fixed-size arrays would supply.
fn av1_grain_coeff(coeffs : Array[Int], index : Int) -> Int {
  if index >= 0 && index < coeffs.length() {
    coeffs[index] - 128
  } else {
    -128
  }
}

///|
priv struct Av1GrainState {
  fg : Av1FilmGrainParams
  bit_depth : Int
  sub_x : Int
  sub_y : Int
  grain_min : Int
  grain_max : Int
  monochrome : Bool
  num_planes : Int
  mut random_register : Int
  /// Row-major 73x82 grain blocks; chroma keeps the same stride.
  luma_grain : Array[Int]
  cb_grain : Array[Int]
  cr_grain : Array[Int]
  /// Per-plane 256-entry scaling lookup tables, flattened.
  scaling : Array[Int]
}

///|
/// The 16-bit LFSR from section 7.18.3.1. `bits` selects the high bits of the
/// advanced register.
fn Av1GrainState::get_random_number(self : Av1GrainState, bits : Int) -> Int {
  let r = self.random_register
  let bit = ((r >> 0) ^ (r >> 1) ^ (r >> 3) ^ (r >> 12)) & 1
  let next = (r >> 1) | (bit << 15)
  let result = (next >> (16 - bits)) & ((1 << bits) - 1)
  self.random_register = next
  result
}

///|
/// Gaussian grain blocks plus the autoregressive pass (section 7.18.3.3).
fn Av1GrainState::generate(self : Av1GrainState) -> Unit {
  let fg = self.fg
  let shift = 12 - self.bit_depth + fg.grain_scale_shift
  self.random_register = fg.grain_seed
  for y in 0..<73 {
    for x in 0..<82 {
      let v = if fg.num_y_points > 0 {
        av1_gaussian_sequence[self.get_random_number(11)]
      } else {
        0
      }
      self.luma_grain[y * 82 + x] = av1_grain_round2(v, shift)
    }
  }
  let ar_shift = fg.ar_coeff_shift_minus6 + 6
  let lag = fg.ar_coeff_lag
  for y in 3..<73 {
    for x in 3..<79 {
      let mut s = 0
      let mut pos = 0
      let mut done = false
      let mut dr = 0 - lag
      while dr <= 0 && !done {
        let mut dc = 0 - lag
        while dc <= lag {
          if dr == 0 && dc == 0 {
            // The centre tap is never applied to luma; it terminates the scan.
            done = true
            break
          }
          let c = av1_grain_coeff(fg.ar_coeffs_y, pos)
          s = s + self.luma_grain[(y + dr) * 82 + (x + dc)] * c
          pos = pos + 1
          dc = dc + 1
        }
        dr = dr + 1
      }
      let idx = y * 82 + x
      self.luma_grain[idx] = av1_grain_clip3(
        self.grain_min,
        self.grain_max,
        self.luma_grain[idx] + av1_grain_round2(s, ar_shift),
      )
    }
  }
  if self.monochrome {
    return
  }
  let chroma_w = if self.sub_x == 1 { 44 } else { 82 }
  let chroma_h = if self.sub_y == 1 { 38 } else { 73 }
  self.random_register = fg.grain_seed ^ 0xb524
  for y in 0.. 0 || fg.chroma_scaling_from_luma {
        av1_gaussian_sequence[self.get_random_number(11)]
      } else {
        0
      }
      self.cb_grain[y * 82 + x] = av1_grain_round2(v, shift)
    }
  }
  self.random_register = fg.grain_seed ^ 0x49d8
  for y in 0.. 0 || fg.chroma_scaling_from_luma {
        av1_gaussian_sequence[self.get_random_number(11)]
      } else {
        0
      }
      self.cr_grain[y * 82 + x] = av1_grain_round2(v, shift)
    }
  }
  for y in 3.. 0 {
              let luma_x = ((x - 3) << self.sub_x) + 3
              let luma_y = ((y - 3) << self.sub_y) + 3
              let mut luma = 0
              for i in 0..<(self.sub_y + 1) {
                for j in 0..<(self.sub_x + 1) {
                  luma = luma +
                    self.luma_grain[(luma_y + i) * 82 + (luma_x + j)]
                }
              }
              luma = av1_grain_round2(luma, self.sub_x + self.sub_y)
              s0 = s0 + luma * c0
              s1 = s1 + luma * c1
            }
            done = true
            break
          }
          s0 = s0 + self.cb_grain[(y + dr) * 82 + (x + dc)] * c0
          s1 = s1 + self.cr_grain[(y + dr) * 82 + (x + dc)] * c1
          pos = pos + 1
          dc = dc + 1
        }
        dr = dr + 1
      }
      let idx = y * 82 + x
      self.cb_grain[idx] = av1_grain_clip3(
        self.grain_min,
        self.grain_max,
        self.cb_grain[idx] + av1_grain_round2(s0, ar_shift),
      )
      self.cr_grain[idx] = av1_grain_clip3(
        self.grain_min,
        self.grain_max,
        self.cr_grain[idx] + av1_grain_round2(s1, ar_shift),
      )
    }
  }
}

///|
/// Interpolate the per-plane scaling function (section 7.18.3.5).
fn Av1GrainState::init_scaling(self : Av1GrainState) -> Unit {
  let fg = self.fg
  for plane in 0..> 1)) / delta_x)
      for x in 0..> 16)
        self.scaling[base + xs[i] + x] = v
      }
    }
    let last_x = xs[num_points - 1]
    let last_y = ys[num_points - 1]
    for x in last_x..<256 {
      self.scaling[base + x] = last_y
    }
  }
}

///|
/// Sample the scaling table, interpolating between entries for high bit depths.
fn Av1GrainState::scale_lut(
  self : Av1GrainState,
  plane : Int,
  index : Int,
) -> Int {
  let shift = self.bit_depth - 8
  let x = index >> shift
  let rem = index - (x << shift)
  let base = plane * 256
  if self.bit_depth == 8 || x >= 255 {
    self.scaling[base + x]
  } else {
    let start = self.scaling[base + x]
    let end = self.scaling[base + x + 1]
    start + av1_grain_round2((end - start) * rem, shift)
  }
}

///|
/// Build the noise image from pseudo-random grain blocks and add it to the
/// frame (section 7.18.3.6). The 32-row luma stripes (16 chroma rows) are
/// streamed: only the previous stripe is retained for the vertical overlap,
/// which keeps the working set proportional to one stripe instead of the
/// whole picture.
fn Av1GrainState::add_noise(
  self : Av1GrainState,
  planes : Array[Array[Int]],
  w : Int,
  h : Int,
  matrix_coefficients : Int,
) -> Unit {
  let fg = self.fg
  let sub_x = self.sub_x
  let sub_y = self.sub_y
  let overlap = fg.overlap_flag
  let gmin = self.grain_min
  let gmax = self.grain_max
  let plane_sub_x = fn(plane : Int) -> Int { if plane > 0 { sub_x } else { 0 } }
  let plane_sub_y = fn(plane : Int) -> Int { if plane > 0 { sub_y } else { 0 } }
  let grain_at = fn(plane : Int, y : Int, x : Int) -> Int {
    if plane == 0 {
      self.luma_grain[y * 82 + x]
    } else if plane == 1 {
      self.cb_grain[y * 82 + x]
    } else {
      self.cr_grain[y * 82 + x]
    }
  }
  let mut num_stripes = 0
  let mut probe = 0
  while probe < (h + 1) / 2 {
    num_stripes = num_stripes + 1
    probe = probe + 16
  }
  if num_stripes <= 0 {
    return
  }
  // Stripe geometry per plane: 32 rows (16 for vertically subsampled chroma)
  // extended by two (one for subsampled) overlap rows.
  let stripe_rows : Array[Int] = []
  let stripe_height : Array[Int] = []
  let stripe_width : Array[Int] = []
  for plane in 0..> psy)
    stripe_height.push(1 << (5 - psy))
    stripe_width.push(((w + psx) >> psx) + 64)
  }
  let plane_w : Array[Int] = []
  let plane_h : Array[Int] = []
  for plane in 0..> psx)
    plane_h.push((h + psy) >> psy)
  }
  let noise : Array[Array[Int]] = []
  for plane in 0..> 4
      let offset_y = rand & 15
      for plane in 0..> psy) {
          for j in 0..<(34 >> psx) {
            let mut gg = grain_at(plane, off_y + i, off_x + j)
            if psx == 0 {
              let idx = x * 2 + j
              if j < 2 && overlap && x > 0 {
                let old = dest[i * dest_w + idx]
                gg = if j == 0 {
                  old * 27 + gg * 17
                } else {
                  old * 17 + gg * 27
                }
                gg = av1_grain_clip3(gmin, gmax, av1_grain_round2(gg, 5))
              }
              dest[i * dest_w + idx] = gg
            } else {
              let idx = x + j
              if j == 0 && overlap && x > 0 {
                let old = dest[i * dest_w + idx]
                gg = old * 23 + gg * 22
                gg = av1_grain_clip3(gmin, gmax, av1_grain_round2(gg, 5))
              }
              dest[i * dest_w + idx] = gg
            }
          }
        }
      }
      x = x + 16
    }
    // Blend this stripe's rows into the noise image, overlapping vertically
    // with the previous stripe when the flag is set.
    for plane in 0..= plane_h[plane] {
          continue
        }
        for x in 0.. 0 && overlap {
              let old = prev[plane][(i + 32) * stripe_w + x]
              gg = if i == 0 { old * 27 + gg * 17 } else { old * 17 + gg * 27 }
              gg = av1_grain_clip3(gmin, gmax, av1_grain_round2(gg, 5))
            }
          } else if i < 1 && luma_num > 0 && overlap {
            let old = prev[plane][(i + 16) * stripe_w + x]
            gg = old * 23 + gg * 22
            gg = av1_grain_clip3(gmin, gmax, av1_grain_round2(gg, 5))
          }
          dest[y * dest_w + x] = gg
        }
      }
    }
    let swap = prev
    prev = cur
    cur = swap
    luma_num = luma_num + 1
    sy = sy + 16
  }
  // Final blend with the reconstructed samples.
  let bd_shift = self.bit_depth - 8
  let full_max = (256 << bd_shift) - 1
  let min_value = if fg.clip_to_restricted_range { 16 << bd_shift } else { 0 }
  let max_luma = if fg.clip_to_restricted_range {
    235 << bd_shift
  } else {
    full_max
  }
  let max_chroma = if !fg.clip_to_restricted_range {
    full_max
  } else if matrix_coefficients == 0 {
    max_luma
  } else {
    240 << bd_shift
  }
  let sample_max = (1 << self.bit_depth) - 1
  let scaling_shift = fg.grain_scaling_minus8 + 8
  let out_y = planes[0]
  let chroma_active = self.num_planes > 1 &&
    (
      fg.num_cb_points > 0 ||
      fg.num_cr_points > 0 ||
      fg.chroma_scaling_from_luma
    )
  if chroma_active {
    let cw = (w + sub_x) >> sub_x
    let ch = (h + sub_y) >> sub_y
    let out_u = planes[1]
    let out_v = planes[2]
    for y in 0.. 0 || fg.chroma_scaling_from_luma {
          let orig = out_u[y * cw + x]
          let mut merged = average_luma
          if !fg.chroma_scaling_from_luma {
            let combined = average_luma * (fg.cb_luma_mult - 128) +
              orig * (fg.cb_mult - 128)
            merged = av1_grain_clip3(
              0,
              sample_max,
              (combined >> 6) + ((fg.cb_offset - 256) << bd_shift),
            )
          }
          let n = av1_grain_round2(
            self.scale_lut(1, merged) * noise[1][y * cw + x],
            scaling_shift,
          )
          out_u[y * cw + x] = av1_grain_clip3(min_value, max_chroma, orig + n)
        }
        if fg.num_cr_points > 0 || fg.chroma_scaling_from_luma {
          let orig = out_v[y * cw + x]
          let mut merged = average_luma
          if !fg.chroma_scaling_from_luma {
            let combined = average_luma * (fg.cr_luma_mult - 128) +
              orig * (fg.cr_mult - 128)
            merged = av1_grain_clip3(
              0,
              sample_max,
              (combined >> 6) + ((fg.cr_offset - 256) << bd_shift),
            )
          }
          let n = av1_grain_round2(
            self.scale_lut(2, merged) * noise[2][y * cw + x],
            scaling_shift,
          )
          out_v[y * cw + x] = av1_grain_clip3(min_value, max_chroma, orig + n)
        }
      }
    }
  }
  if fg.num_y_points > 0 {
    for y in 0.. Unit? {
  if !fg.apply_grain {
    return Some(())
  }
  if width <= 0 || height <= 0 || planes.length() == 0 {
    return None
  }
  if bit_depth != 8 && bit_depth != 10 && bit_depth != 12 {
    return None
  }
  let num_planes = if monochrome { 1 } else { 3 }
  if planes.length() < num_planes {
    return None
  }
  let sub_x = if monochrome || subsampling_x { 1 } else { 0 }
  let sub_y = if monochrome || subsampling_y { 1 } else { 0 }
  let grain_center = 128 << (bit_depth - 8)
  let state : Av1GrainState = {
    fg,
    bit_depth,
    sub_x,
    sub_y,
    grain_min: 0 - grain_center,
    grain_max: (256 << (bit_depth - 8)) - 1 - grain_center,
    monochrome,
    num_planes,
    random_register: 0,
    luma_grain: Array::make(73 * 82, 0),
    cb_grain: Array::make(73 * 82, 0),
    cr_grain: Array::make(73 * 82, 0),
    scaling: Array::make(3 * 256, 0),
  }
  state.generate()
  state.init_scaling()
  state.add_noise(planes, width, height, matrix_coefficients)
  Some(())
}