///|
/// DCT 域不可见水印:8×8 分块 DCT-II,中频系数 QIM(量化索引调制)嵌入。
///
/// 与 LSB 的取舍:LSB 抗无损但被 JPEG 量化破坏;DCT 域将 bit 编码进
/// 中频 DCT 系数的量化网格,JPEG 重压后系数仍在网格内即可提取——
/// 实测:纯色/低纹理图 q60 往返可提取(见 `dct_wbtest.mbt`);自然纹理
/// 图 q85 默认 `delta=24` 通过,强压缩 q60 需 `delta=48`(见 CLI `--delta`)。
///
/// 布局(逐块 1 bit,按行主序):[magic 2B "MD"] [len 2B] [payload]。
/// 嵌入系数固定为 (u=4, v=1)(中频、JPEG 量化步长适中);量化步长
/// `delta` 默认 24,越大越鲁棒、可见性略升。

///|
/// 将 `payload` 以 DCT 域方式嵌入(原地修改)。容量 = 完整 8×8 块数 / 8 字节。
/// 量化步长默认值(QIM 网格);`delta?` 未提供时取 24。embed/extract/detect 共用。
fn default_delta(delta : Int32?) -> Int32 {
  match delta {
    Some(v) => v
    None => 24
  }
}

///|
pub fn RgbaImage::embed_dct(
  self : RgbaImage,
  payload : Bytes,
  delta? : Int32,
) -> Result[RgbaImage, String] {
  let d = default_delta(delta)
  if d <= 0 {
    return Err("delta must be > 0, got \{d}")
  }
  if payload.length() > 65535 {
    // 帧头长度字段为 2 字节大端,>65535 会被静默截断(66000→464),直接拒绝
    return Err("payload too large: \{payload.length()}B > 65535B (2-byte length field)")
  }
  let blocks = self.width / 8 * (self.height / 8)
  let need = 4 + payload.length()
  if need * 8 > blocks {
    return Err("payload too large: \{need}B > \{blocks / 8}B capacity")
  }
  // 明文帧:magic + len + payload(与 LSB 共用的帧布局,见 invisible.mbt `make_frame`)
  let frame = make_frame(b'M', b'D', payload)
  let mut bi = 0
  let total = need * 8
  for by in 0..<(self.height / 8) {
    if bi >= total {
      break
    }
    for bx in 0..<(self.width / 8) {
      if bi >= total {
        break
      }
      let bit = (frame[bi / 8].to_int() >> (bi % 8)) & 1
      self.embed_dct_bit(bx * 8, by * 8, bit, d)
      bi += 1
    }
  }
  Ok(self)
}

///|
/// 从图像提取 DCT 域水印;magic 校验失败返回 `None`。
pub fn extract_dct(img : RgbaImage, delta? : Int32) -> Bytes? {
  let d = default_delta(delta)
  if d <= 0 {
    return None
  }
  let blocks = img.width / 8 * (img.height / 8)
  match read_dct_head(img, d) {
    None => None
    Some((m0, m1, len)) => {
      let need = 4 + len
      if need * 8 > blocks {
        return None
      }
      // 用已还原的帧头重建明文帧,再读 payload 位
      let frame = FixedArray::make(need, (0).to_byte())
      frame[0] = m0
      frame[1] = m1
      frame[2] = ((len >> 8) & 0xFF).to_byte()
      frame[3] = (len & 0xFF).to_byte()
      fill_bits(
        frame,
        fn(bi) {
          let (x0, y0) = block_origin(img, bi)
          extract_dct_bit(img, x0, y0, d)
        },
        32,
        need * 8,
      )
      let out = Array::make(len, (0).to_byte())
      for i in 0.. Unit {
  // 取亮度 Y(BT.601)到块矩阵——与 JPEG 编码的亮度分量一致(见 `block_luma`)
  let f = block_luma(self, x0, y0)
  let fk = dct_2d(f)
  // QIM:目标网格奇偶 = bit(偶=0,奇=1);与原始网格至多差 1 格
  let q0 = (fk[4][1] / delta.to_double()).round().to_int()
  let tq = if bit == 1 { q0 | 1 } else { q0 & -2 }
  fk[4][1] = tq.to_double() * delta.to_double()
  let back = idct_2d(fk)
  for n in 0..<8 {
    for m in 0..<8 {
      // 亮度差 Δ 均摊到 RGB 三通道(同增 Δ → Y 增 Δ,色相近似保持)
      let d = back[n][m] - f[n][m]
      let p = self.get_pixel(x0 + m, y0 + n)
      let r = ((p >> 24) & 0xFF).to_double() + d
      let g = ((p >> 16) & 0xFF).to_double() + d
      let b = ((p >> 8) & 0xFF).to_double() + d
      let rv = if r < 0.0 { 0.0 } else if r > 255.0 { 255.0 } else { r }
      let gv = if g < 0.0 { 0.0 } else if g > 255.0 { 255.0 } else { g }
      let bv = if b < 0.0 { 0.0 } else if b > 255.0 { 255.0 } else { b }
      let np = (rv.to_int() << 24) |
        (gv.to_int() << 16) |
        (bv.to_int() << 8) |
        (p & 0xFF)
      self.set_pixel(x0 + m, y0 + n, np)
    }
  }
}

///|
/// 提取 1 bit:亮度域系数网格奇偶判定。
fn extract_dct_bit(
  img : RgbaImage,
  x0 : Int32,
  y0 : Int32,
  delta : Int32,
) -> Int32 {
  let f = block_luma(img, x0, y0)
  let fk = dct_2d(f)
  let q0 = (fk[4][1] / delta.to_double()).round().to_int()
  q0 & 1
}

///|
/// 构造 8×8 独立行矩阵(FixedArray/Array 嵌套 make 会共享引用,必须逐行新建)。
fn dct8() -> Array[FixedArray[Double]] {
  let rows = Array(capacity=8)
  for _ in 0..<8 {
    rows.push(FixedArray::make(8, 0.0))
  }
  rows
}

///|
/// 预计算 8×8 余弦表(cos(π(2n+1)u/16)),模块加载时只算一次。
fn build_cos_table() -> Array[FixedArray[Double]] {
  let t = dct8()
  for u in 0..<8 {
    for n in 0..<8 {
      t[u][n] = @math.cos(PI * ((2 * n + 1) * u).to_double() / 16.0)
    }
  }
  t
}

let cos_table : Array[FixedArray[Double]] = build_cos_table()

///|
/// 8×8 DCT-II 正变换。
fn dct_2d(f : Array[FixedArray[Double]]) -> Array[FixedArray[Double]] {
  let out = dct8()
  for u in 0..<8 {
    for v in 0..<8 {
      let mut s = 0.0
      for n in 0..<8 {
        for m in 0..<8 {
          s = s + f[n][m] * cos_t(u, n) * cos_t(v, m)
        }
      }
      out[u][v] = 0.25 * dct_alpha(u) * dct_alpha(v) * s
    }
  }
  out
}

///|
/// 8×8 DCT-II 逆变换。
fn idct_2d(fk : Array[FixedArray[Double]]) -> Array[FixedArray[Double]] {
  let out = dct8()
  for n in 0..<8 {
    for m in 0..<8 {
      let mut s = 0.0
      for u in 0..<8 {
        for v in 0..<8 {
          s = s +
            dct_alpha(u) * dct_alpha(v) * fk[u][v] * cos_t(u, n) * cos_t(v, m)
        }
      }
      out[n][m] = 0.25 * s
    }
  }
  out
}

///|
/// DCT 基函数:cos(π(2n+1)u/16),查模块级预计算表。
///
/// dct_2d/idct_2d 内层每个 8×8 块要调用上万次 cos_t,但该函数只依赖
/// u,n ∈ [0,8),共 64 个固定值;模块加载时算一次,后续 O(1) 查表。
fn cos_t(u : Int32, n : Int32) -> Double {
  cos_table[u][n]
}

///|
/// 归一化因子:u=0 时 1/√2,否则 1。
fn dct_alpha(u : Int32) -> Double {
  if u == 0 {
    1.0 / SQRT2
  } else {
    1.0
  }
}

///|
/// 块位流下标 → 8×8 块左上角坐标(行主序)。embed 的行列推进与
/// extract/detect 的下标换算共用同一公式,避免两侧坐标漂移。
fn block_origin(img : RgbaImage, bi : Int32) -> (Int32, Int32) {
  let cols = img.width / 8
  (bi % cols * 8, bi / cols * 8)
}

///|
/// 盲检测:用给定 `delta` 读 DCT 帧头(magic + 长度合理性)判断是否含水印。
///
/// `delta` 需与嵌入时一致(可选,默认 24)——detect 与 extract 共用同一
/// 量化网格判定,两侧不一致会检不出(返回 false,属正常语义而非 bug)。
/// 用于拿疑似泄漏帧先判断是否被 DCT 标记,再决定深入提取。
pub fn detect_dct(img : RgbaImage, delta? : Int32) -> Bool {
  let d = default_delta(delta)
  if d <= 0 {
    return false
  }
  let blocks = img.width / 8 * (img.height / 8)
  match read_dct_head(img, d) {
    None => false
    Some((_, _, len)) => 4 + len <= blocks / 8
  }
}

///|
/// 读取 DCT 帧头(magic + 长度):容量不足或 magic 不匹配返回 `None`。
/// `extract_dct` 与 `detect_dct` 共用——两侧头部解析必须同一套逻辑。
fn read_dct_head(img : RgbaImage, delta : Int32) -> (Byte, Byte, Int32)? {
  let blocks = img.width / 8 * (img.height / 8)
  if blocks < 32 {
    return None // 至少 4 字节头部
  }
  let head = FixedArray::make(4, (0).to_byte())
  fill_bits(
    head,
    fn(bi) {
      let (x0, y0) = block_origin(img, bi)
      extract_dct_bit(img, x0, y0, delta)
    },
    0,
    32,
  )
  if head[0] != b'M' || head[1] != b'D' {
    return None
  }
  Some((head[0], head[1], (head[2].to_int() << 8) | head[3].to_int()))
}

///|
/// 取 8×8 块的亮度矩阵 Y = 0.299R + 0.587G + 0.114B(BT.601)。
///
/// 与 JPEG 编码的亮度分量一致,DCT 系数才能经受 JPEG 量化往返
/// (G 通道会被色度量化破坏);embed 与 extract 两侧共用,保证公式不漂移。
fn block_luma(
  img : RgbaImage,
  x0 : Int32,
  y0 : Int32,
) -> Array[FixedArray[Double]] {
  let f = dct8()
  for n in 0..<8 {
    for m in 0..<8 {
      let p = img.get_pixel(x0 + m, y0 + n)
      let r = (p >> 24) & 0xFF
      let g = (p >> 16) & 0xFF
      let b = (p >> 8) & 0xFF
      f[n][m] = 0.299 * r.to_double() +
        0.587 * g.to_double() +
        0.114 * b.to_double()
    }
  }
  f
}

///|
/// 查询 DCT 通道可用 payload 容量(字节):块数/8 再扣除 4 字节帧头。
///
/// 返回 0 或负数表示放不下(需更大的图或改走 LSB);容量与 `embed_dct`
/// 的校验口径一致(`4 + len <= blocks / 8`)。
pub fn RgbaImage::dct_capacity(self : RgbaImage) -> Int32 {
  self.width / 8 * (self.height / 8) / 8 - 4
}