// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// SPDX-License-Identifier: GPL-2.0-or-later

//! OSD-1/OSD-2 CRC-guided bit-flip decoder for the (174,91) LDPC code.
//!
//! This is a port of `ft2_ldpc.c` which implements Ordered Statistics Decoding
//! with configurable depth (ndeep 0-6). The decoder first runs iterative
//! belief-propagation (BP), then falls back to OSD refinement using the
//! accumulated LLR sums from BP iterations.
//!
//! The OSD algorithm works by:
//! 1. Sorting codeword bits by LLR reliability
//! 2. Gaussian elimination to put the generator matrix in systematic form
//!    (with respect to the most reliable bits)
//! 3. Exhaustive search over bit-flip patterns of increasing weight
//! 4. Pattern hashing (OSD-2) to efficiently search two-bit-flip corrections

use std::sync::OnceLock;

use super::constants::{FTX_LDPC_GENERATOR, FTX_LDPC_MN, FTX_LDPC_NM, FTX_LDPC_NUM_ROWS};
use super::crc::{ftx_compute_crc, ftx_extract_crc};
use super::decode::pack_bits;
use super::encode::parity8;
use super::ldpc::ldpc_check;
use super::protocol::{FTX_LDPC_K, FTX_LDPC_K_BYTES, FTX_LDPC_M, FTX_LDPC_N};

/// Piecewise linear approximation of `atanh(x)` used in BP message passing.
fn platanh(x: f32) -> f32 {
    let isign: f32 = if x < 0.0 { -1.0 } else { 1.0 };
    let z = x.abs();

    if z <= 0.664 {
        return x / 0.83;
    }
    if z <= 0.9217 {
        return isign * ((z - 0.4064) / 0.322);
    }
    if z <= 0.9951 {
        return isign * ((z - 0.8378) / 0.0524);
    }
    if z <= 0.9998 {
        return isign * ((z - 0.9914) / 0.0012);
    }
    isign * 7.0
}

/// Check CRC of a 91-bit message (in bit array form).
fn check_crc91(plain91: &[u8]) -> bool {
    let mut a91 = [0u8; FTX_LDPC_K_BYTES];
    pack_bits(plain91, FTX_LDPC_K, &mut a91);
    let crc_extracted = ftx_extract_crc(&a91);
    a91[9] &= 0xF8;
    a91[10] = 0x00;
    let crc_calculated = ftx_compute_crc(&a91, 96 - 14);
    crc_extracted == crc_calculated
}

/// Encode a 91-bit message (bit array) into a 174-bit codeword without CRC computation.
fn encode174_91_nocrc_bits(message91: &[u8], codeword: &mut [u8; FTX_LDPC_N]) {
    let mut packed = [0u8; FTX_LDPC_K_BYTES];
    pack_bits(message91, FTX_LDPC_K, &mut packed);

    // Systematic bits
    for i in 0..FTX_LDPC_K {
        codeword[i] = message91[i] & 0x01;
    }

    // Parity bits from generator matrix
    for i in 0..FTX_LDPC_M {
        let mut nsum: u8 = 0;
        for j in 0..FTX_LDPC_K_BYTES {
            nsum ^= parity8(packed[j] & FTX_LDPC_GENERATOR[i][j]);
        }
        codeword[FTX_LDPC_K + i] = nsum & 0x01;
    }
}

/// Matrix-vector multiply for re-encoding in OSD.
fn mrbencode91(me: &[u8], codeword: &mut [u8], g2: &[u8], n: usize, k: usize) {
    codeword[..n].fill(0);
    for i in 0..k {
        if me[i] == 0 {
            continue;
        }
        codeword[..n]
            .iter_mut()
            .enumerate()
            .for_each(|(j, c)| *c ^= g2[j * k + i]);
    }
}

/// Generate next bit-flip pattern of given order.
fn nextpat91(mi: &mut [u8], k: usize, iorder: usize, iflag: &mut i32) {
    let mut ind: i32 = -1;
    for i in 0..k.saturating_sub(1) {
        if mi[i] == 0 && mi[i + 1] == 1 {
            ind = i as i32;
        }
    }

    if ind < 0 {
        *iflag = -1;
        return;
    }

    // Build new pattern in-place: zero out after ind, set the swap, pack remaining 1s at end
    let ind_u = ind as usize;
    mi[(ind_u + 1)..k].fill(0);
    mi[ind_u] = 1;

    let mut nz = iorder as i32;
    for &v in mi.iter().take(k) {
        nz -= v as i32;
    }
    if nz > 0 {
        mi[(k - nz as usize)..k].fill(1);
    }

    *iflag = -1;
    for (i, &v) in mi.iter().enumerate().take(k) {
        if v == 1 {
            *iflag = i as i32;
            break;
        }
    }
}

/// Pattern hash table for OSD-2 optimization.
struct OsdBox {
    head: Vec<i32>,
    next: Vec<i32>,
    pairs: Vec<[i32; 2]>,
    capacity: usize,
    count: usize,
    last_pattern: i32,
    next_index: i32,
}

impl OsdBox {
    fn new(ntau: usize) -> Option<Self> {
        let size = 1 << ntau;
        let capacity = 5000;
        Some(Self {
            head: vec![-1; size],
            next: vec![-1; capacity],
            pairs: vec![[-1, -1]; capacity],
            capacity,
            count: 0,
            last_pattern: -1,
            next_index: -1,
        })
    }

    fn boxit(&mut self, e2: &[u8], ntau: usize, i1: i32, i2: i32) {
        if self.count >= self.capacity {
            return;
        }
        let idx = self.count;
        self.count += 1;
        self.pairs[idx] = [i1, i2];

        let ipat = pattern_hash(e2, ntau);
        let ip = self.head[ipat];
        if ip == -1 {
            self.head[ipat] = idx as i32;
        } else {
            let mut cur = ip;
            while self.next[cur as usize] != -1 {
                cur = self.next[cur as usize];
            }
            self.next[cur as usize] = idx as i32;
        }
    }

    fn fetchit(&mut self, e2: &[u8], ntau: usize) -> (i32, i32) {
        let ipat = pattern_hash(e2, ntau);
        let index = self.head[ipat];

        if self.last_pattern != ipat as i32 && index >= 0 {
            let i1 = self.pairs[index as usize][0];
            let i2 = self.pairs[index as usize][1];
            self.next_index = self.next[index as usize];
            self.last_pattern = ipat as i32;
            (i1, i2)
        } else if self.last_pattern == ipat as i32 && self.next_index >= 0 {
            let ni = self.next_index as usize;
            let i1 = self.pairs[ni][0];
            let i2 = self.pairs[ni][1];
            self.next_index = self.next[ni];
            (i1, i2)
        } else {
            self.next_index = -1;
            self.last_pattern = ipat as i32;
            (-1, -1)
        }
    }
}

/// Compute hash of a bit pattern for OSD-2 lookup.
fn pattern_hash(e2: &[u8], ntau: usize) -> usize {
    let mut ipat = 0usize;
    for (i, &v) in e2.iter().enumerate().take(ntau) {
        if v != 0 {
            ipat |= 1 << (ntau - i - 1);
        }
    }
    ipat
}

/// Ordered Statistics Decoding with configurable depth.
///
/// `llr`: log-likelihood ratios for 174 bits (modified internally).
/// `k`: number of systematic bits (91).
/// `apmask`: a priori mask (which bits are known).
/// `ndeep`: search depth (0-6).
/// `message91`: output 91-bit message.
/// `cw`: output 174-bit codeword.
/// `nhardmin`: output minimum hard errors.
/// `dmin`: output minimum distance.
#[allow(clippy::too_many_arguments)]
pub fn osd174_91(
    llr: &mut [f32; FTX_LDPC_N],
    k: usize,
    apmask: &[u8; FTX_LDPC_N],
    ndeep: usize,
    message91: &mut [u8; FTX_LDPC_K],
    cw: &mut [u8; FTX_LDPC_N],
    nhardmin: &mut i32,
    dmin: &mut f32,
) {
    let n = FTX_LDPC_N;
    let ndeep = ndeep.min(6);

    // Cached per-bit generator matrix (each row i generates codeword from
    // unit vector e_i)
    let gen = generator_matrix();

    // Stack-allocated working buffers (k=91, n=174, n-k=83).
    let mut genmrb = [0u8; FTX_LDPC_K * FTX_LDPC_N];
    let mut g2 = [0u8; FTX_LDPC_N * FTX_LDPC_K];
    let mut m0 = [0u8; FTX_LDPC_K];
    let mut me = [0u8; FTX_LDPC_K];
    let mut mi = [0u8; FTX_LDPC_K];
    let mut misub = [0u8; FTX_LDPC_K];
    let mut e2sub = [0u8; FTX_LDPC_M];
    let mut e2 = [0u8; FTX_LDPC_M];
    let mut ui = [0u8; FTX_LDPC_M];
    let mut r2pat = [0u8; FTX_LDPC_M];
    let mut hdec = [0u8; FTX_LDPC_N];
    let mut c0 = [0u8; FTX_LDPC_N];
    let mut ce = [0u8; FTX_LDPC_N];
    let mut nxor = [0u8; FTX_LDPC_N];
    let mut apmaskr = [0u8; FTX_LDPC_N];
    let mut rx = [0.0f32; FTX_LDPC_N];
    let mut absrx = [0.0f32; FTX_LDPC_N];
    let mut indices = [0usize; FTX_LDPC_N];

    // Sort bits by reliability (descending)
    let mut rel_indices = [0usize; FTX_LDPC_N];
    let mut rel_abs = [0.0f32; FTX_LDPC_N];
    for i in 0..n {
        rel_indices[i] = i;
        rel_abs[i] = llr[i].abs();
    }
    rel_indices[..n].sort_by(|&a, &b| {
        rel_abs[b]
            .partial_cmp(&rel_abs[a])
            .unwrap_or(std::cmp::Ordering::Equal)
    });

    for i in 0..n {
        rx[i] = llr[i];
        apmaskr[i] = apmask[i];
        hdec[i] = if rx[i] >= 0.0 { 1 } else { 0 };
        absrx[i] = rx[i].abs();
    }

    // Reorder by reliability
    for i in 0..n {
        indices[i] = rel_indices[i];
        for row in 0..k {
            genmrb[row * n + i] = gen[row][indices[i]];
        }
    }

    // Gaussian elimination to systematic form
    for id in 0..k {
        let max_col = (k + 20).min(n);
        for col in id..max_col {
            if genmrb[id * n + col] == 0 {
                continue;
            }
            // Swap columns id and col
            if col != id {
                for row in 0..k {
                    genmrb.swap(row * n + id, row * n + col);
                }
                indices.swap(id, col);
            }
            // Eliminate column id from all other rows
            for row in 0..k {
                if row != id && genmrb[row * n + id] == 1 {
                    for c in 0..n {
                        genmrb[row * n + c] ^= genmrb[id * n + c];
                    }
                }
            }
            break;
        }
    }

    // Transpose to column-major g2
    for row in 0..k {
        for col in 0..n {
            g2[col * k + row] = genmrb[row * n + col];
        }
    }

    // Reorder LLRs and hard decisions by reliability
    for i in 0..n {
        hdec[i] = if rx[indices[i]] >= 0.0 { 1 } else { 0 };
        absrx[i] = rx[indices[i]].abs();
        rx[i] = llr[indices[i]];
        apmaskr[i] = apmask[indices[i]];
    }
    m0[..k].copy_from_slice(&hdec[..k]);

    // Initial encode
    mrbencode91(&m0, &mut c0, &g2, n, k);
    for i in 0..n {
        nxor[i] = c0[i] ^ hdec[i];
    }
    *nhardmin = 0;
    *dmin = 0.0;
    for i in 0..n {
        *nhardmin += nxor[i] as i32;
        if nxor[i] != 0 {
            *dmin += absrx[i];
        }
    }
    cw.copy_from_slice(&c0[..n]);

    if ndeep == 0 {
        reorder_result(cw, &indices, message91, nhardmin, dmin, llr);
        return;
    }

    // Configure search parameters based on depth
    let (nord, npre1, npre2, nt, ntheta, ntau) = match ndeep {
        1 => (1, 0, 0, 40, 12, 0),
        2 => (1, 1, 0, 40, 10, 0),
        3 => (1, 1, 1, 40, 12, 14),
        4 => (2, 1, 1, 40, 12, 17),
        5 => (3, 1, 1, 40, 12, 15),
        _ => (4, 1, 1, 95, 12, 15),
    };

    // OSD-1: exhaustive search over bit patterns of increasing order
    for iorder in 1..=nord {
        misub.iter_mut().for_each(|v| *v = 0);
        misub[(k - iorder)..k].fill(1);
        let mut iflag = (k - iorder) as i32;

        while iflag >= 0 {
            let iend = if iorder == nord && npre1 == 0 {
                iflag as usize
            } else {
                0
            };
            let mut d1 = 0.0f32;

            let mut n1 = iflag;
            while n1 >= iend as i32 {
                mi[..k].copy_from_slice(&misub[..k]);
                mi[n1 as usize] = 1;

                // Check if any masked bit would be flipped
                let masked = (0..k).any(|i| apmaskr[i] != 0 && mi[i] != 0);
                if masked {
                    n1 -= 1;
                    continue;
                }

                for i in 0..k {
                    me[i] = m0[i] ^ mi[i];
                }

                if n1 == iflag {
                    mrbencode91(&me, &mut ce, &g2, n, k);
                    for i in 0..(n - k) {
                        e2sub[i] = ce[k + i] ^ hdec[k + i];
                        e2[i] = e2sub[i];
                    }
                    let mut nd1kpt = 1;
                    for &v in e2sub.iter().take(nt.min(n - k)) {
                        nd1kpt += v as i32;
                    }
                    d1 = 0.0;
                    for i in 0..k {
                        if (me[i] ^ hdec[i]) != 0 {
                            d1 += absrx[i];
                        }
                    }
                    if nd1kpt <= ntheta {
                        let mut dd = d1;
                        for i in 0..(n - k) {
                            if e2sub[i] != 0 {
                                dd += absrx[k + i];
                            }
                        }
                        if dd < *dmin {
                            *dmin = dd;
                            cw[..n].copy_from_slice(&ce[..n]);
                            *nhardmin = 0;
                            for i in 0..n {
                                *nhardmin += (ce[i] ^ hdec[i]) as i32;
                            }
                        }
                    }
                } else {
                    for i in 0..(n - k) {
                        e2[i] = e2sub[i] ^ g2[(k + i) * k + n1 as usize];
                    }
                    let mut nd1kpt = 2;
                    for &v in e2.iter().take(nt.min(n - k)) {
                        nd1kpt += v as i32;
                    }
                    if nd1kpt <= ntheta {
                        mrbencode91(&me, &mut ce, &g2, n, k);
                        let mut dd = d1
                            + if (ce[n1 as usize] ^ hdec[n1 as usize]) != 0 {
                                absrx[n1 as usize]
                            } else {
                                0.0
                            };
                        for i in 0..(n - k) {
                            if e2[i] != 0 {
                                dd += absrx[k + i];
                            }
                        }
                        if dd < *dmin {
                            *dmin = dd;
                            cw[..n].copy_from_slice(&ce[..n]);
                            *nhardmin = 0;
                            for i in 0..n {
                                *nhardmin += (ce[i] ^ hdec[i]) as i32;
                            }
                        }
                    }
                }

                n1 -= 1;
            }
            nextpat91(&mut misub, k, iorder, &mut iflag);
        }
    }

    // OSD-2: pattern-hashed two-bit-flip search
    if npre2 == 1 {
        if let Some(mut osd_box) = OsdBox::new(ntau) {
            // Build hash table of all column pairs
            for i1 in (0..k as i32).rev() {
                for i2 in (0..i1).rev() {
                    for i in 0..ntau {
                        mi[i] = g2[(k + i) * k + i1 as usize] ^ g2[(k + i) * k + i2 as usize];
                    }
                    osd_box.boxit(&mi, ntau, i1, i2);
                }
            }

            // Search using base patterns
            misub.iter_mut().for_each(|v| *v = 0);
            misub[(k - nord)..k].fill(1);
            let mut iflag = (k - nord) as i32;

            while iflag >= 0 {
                for i in 0..k {
                    me[i] = m0[i] ^ misub[i];
                }
                mrbencode91(&me, &mut ce, &g2, n, k);
                for i in 0..(n - k) {
                    e2sub[i] = ce[k + i] ^ hdec[k + i];
                }

                for i2 in 0..=ntau {
                    ui.iter_mut().for_each(|v| *v = 0);
                    if i2 > 0 {
                        ui[i2 - 1] = 1;
                    }
                    for i in 0..ntau {
                        r2pat[i] = e2sub[i] ^ ui[i];
                    }

                    osd_box.last_pattern = -1;
                    osd_box.next_index = -1;

                    loop {
                        let (in1, in2) = osd_box.fetchit(&r2pat, ntau);
                        if in1 < 0 || in2 < 0 {
                            break;
                        }

                        mi[..k].copy_from_slice(&misub[..k]);
                        mi[in1 as usize] = 1;
                        mi[in2 as usize] = 1;

                        let mut w = 0;
                        let mut masked = false;
                        for i in 0..k {
                            w += mi[i] as usize;
                            if apmaskr[i] != 0 && mi[i] != 0 {
                                masked = true;
                            }
                        }

                        if w < nord + npre1 + npre2 || masked {
                            continue;
                        }

                        for i in 0..k {
                            me[i] = m0[i] ^ mi[i];
                        }
                        mrbencode91(&me, &mut ce, &g2, n, k);

                        let mut dd = 0.0f32;
                        let mut nh = 0i32;
                        for i in 0..n {
                            let diff = ce[i] ^ hdec[i];
                            nh += diff as i32;
                            if diff != 0 {
                                dd += absrx[i];
                            }
                        }
                        if dd < *dmin {
                            *dmin = dd;
                            cw[..n].copy_from_slice(&ce[..n]);
                            *nhardmin = nh;
                        }
                    }
                }
                nextpat91(&mut misub, k, nord, &mut iflag);
            }
        }
    }

    reorder_result(cw, &indices, message91, nhardmin, dmin, llr);
}

/// Reorder codeword back to original bit ordering and verify CRC.
fn reorder_result(
    cw: &mut [u8; FTX_LDPC_N],
    indices: &[usize],
    message91: &mut [u8; FTX_LDPC_K],
    nhardmin: &mut i32,
    _dmin: &mut f32,
    _llr: &[f32; FTX_LDPC_N],
) {
    let mut reordered = [0u8; FTX_LDPC_N];
    for i in 0..FTX_LDPC_N {
        reordered[indices[i]] = cw[i];
    }
    cw.copy_from_slice(&reordered);
    message91.copy_from_slice(&cw[..FTX_LDPC_K]);
    if !check_crc91(message91) {
        *nhardmin = -*nhardmin;
    }
}

/// Get a reference to the cached generator matrix.
/// The matrix is computed once on first call and reused thereafter.
fn generator_matrix() -> &'static [[u8; FTX_LDPC_N]; FTX_LDPC_K] {
    static GEN: OnceLock<Box<[[u8; FTX_LDPC_N]; FTX_LDPC_K]>> = OnceLock::new();
    GEN.get_or_init(|| {
        let mut gen = Box::new([[0u8; FTX_LDPC_N]; FTX_LDPC_K]);
        for i in 0..FTX_LDPC_K {
            let mut msg = [0u8; FTX_LDPC_K];
            msg[i] = 1;
            if i < 77 {
                msg[77..FTX_LDPC_K].fill(0);
            }
            encode174_91_nocrc_bits(&msg, &mut gen[i]);
        }
        gen
    })
}

/// Full iterative BP decoder with OSD refinement.
///
/// Runs belief-propagation for up to `maxiterations` iterations, saving
/// accumulated LLR sums. If BP does not converge, falls back to OSD
/// using the saved sums.
///
/// `llr`: input log-likelihood ratios (174 values).
/// `keff`: effective K (must be 91).
/// `maxosd`: maximum number of OSD passes (0-3).
/// `norder`: OSD depth parameter.
/// `apmask`: a priori mask.
/// `message91`: output decoded 91-bit message.
/// `cw`: output 174-bit codeword.
/// `ntype`: output decode type (0=fail, 1=BP, 2=OSD).
/// `nharderror`: output number of hard errors.
/// `dmin`: output minimum distance.
#[allow(clippy::too_many_arguments)]
pub fn ft2_decode174_91_osd(
    llr: &mut [f32; FTX_LDPC_N],
    keff: usize,
    maxosd: usize,
    norder: usize,
    apmask: &mut [u8; FTX_LDPC_N],
    message91: &mut [u8; FTX_LDPC_K],
    cw: &mut [u8; FTX_LDPC_N],
    ntype: &mut i32,
    nharderror: &mut i32,
    dmin: &mut f32,
) {
    *ntype = 0;
    *nharderror = -1;
    *dmin = 0.0;

    if keff != FTX_LDPC_K {
        return;
    }

    let maxiterations = 30;
    let maxosd = maxosd.min(3);

    let nosd = if maxosd == 0 { 1 } else { maxosd };

    let mut zsave = [[0.0f32; FTX_LDPC_N]; 3];
    if maxosd == 0 {
        zsave[0].copy_from_slice(llr);
    }

    let mut tov = [[0.0f32; 3]; FTX_LDPC_N];
    let mut toc = [[0.0f32; 7]; FTX_LDPC_M];
    let mut zsum = [0.0f32; FTX_LDPC_N];
    let mut hdec = [0u8; FTX_LDPC_N];
    let mut best_cw = [0u8; FTX_LDPC_N];
    let mut ncnt = 0;
    let mut nclast = 0;

    for iter in 0..=maxiterations {
        // Compute beliefs
        let mut zn = [0.0f32; FTX_LDPC_N];
        for i in 0..FTX_LDPC_N {
            zn[i] = llr[i];
            if apmask[i] != 1 {
                zn[i] += tov[i][0] + tov[i][1] + tov[i][2];
            }
            zsum[i] += zn[i];
        }
        if iter > 0 && iter <= maxosd {
            zsave[iter - 1].copy_from_slice(&zsum);
        }

        // Hard decisions
        for i in 0..FTX_LDPC_N {
            best_cw[i] = if zn[i] > 0.0 { 1 } else { 0 };
        }
        let ncheck = ldpc_check(&best_cw);

        if ncheck == 0 && check_crc91(&best_cw) {
            message91.copy_from_slice(&best_cw[..FTX_LDPC_K]);
            cw.copy_from_slice(&best_cw);
            for i in 0..FTX_LDPC_N {
                hdec[i] = if llr[i] >= 0.0 { 1 } else { 0 };
            }
            *nharderror = 0;
            *dmin = 0.0;
            for i in 0..FTX_LDPC_N {
                let diff = hdec[i] ^ best_cw[i];
                *nharderror += diff as i32;
                if diff != 0 {
                    *dmin += llr[i].abs();
                }
            }
            *ntype = 1;
            return;
        }

        // Early termination
        if iter > 0 {
            let nd = ncheck - nclast;
            ncnt = if nd < 0 { 0 } else { ncnt + 1 };
            if ncnt >= 5 && iter >= 10 && ncheck > 15 {
                *nharderror = -1;
                break;
            }
        }
        nclast = ncheck;

        // Check-to-variable messages
        for m in 0..FTX_LDPC_M {
            let num_rows = FTX_LDPC_NUM_ROWS[m] as usize;
            for n_idx in 0..num_rows {
                let n = FTX_LDPC_NM[m][n_idx] as usize - 1;
                if n >= FTX_LDPC_N {
                    continue;
                }
                toc[m][n_idx] = zn[n];
                for kk in 0..3 {
                    if (FTX_LDPC_MN[n][kk] as usize).wrapping_sub(1) == m {
                        toc[m][n_idx] -= tov[n][kk];
                    }
                }
            }
        }

        // Variable-to-check messages
        for m in 0..FTX_LDPC_M {
            let num_rows = FTX_LDPC_NUM_ROWS[m] as usize;
            let mut tanhtoc = [0.0f32; 7];
            for i in 0..num_rows.min(7) {
                tanhtoc[i] = (-toc[m][i] / 2.0).tanh();
            }
            for &nm_val in FTX_LDPC_NM[m].iter().take(num_rows) {
                let n = nm_val as usize - 1;
                if n >= FTX_LDPC_N {
                    continue;
                }
                let mut tmn = 1.0f32;
                for n_idx in 0..num_rows {
                    if FTX_LDPC_NM[m][n_idx] as usize - 1 != n {
                        tmn *= tanhtoc[n_idx];
                    }
                }
                for kk in 0..3 {
                    if (FTX_LDPC_MN[n][kk] as usize).wrapping_sub(1) == m {
                        tov[n][kk] = 2.0 * platanh(-tmn);
                    }
                }
            }
        }
    }

    // OSD fallback
    for i in 0..nosd {
        if i >= zsave.len() {
            break;
        }
        let mut osd_llr = [0.0f32; FTX_LDPC_N];
        osd_llr.copy_from_slice(&zsave[i]);
        let mut osd_harderror: i32 = -1;
        let mut osd_dmin: f32 = 0.0;
        osd174_91(
            &mut osd_llr,
            keff,
            apmask,
            norder,
            message91,
            cw,
            &mut osd_harderror,
            &mut osd_dmin,
        );
        if osd_harderror > 0 {
            *nharderror = osd_harderror;
            *dmin = 0.0;
            for j in 0..FTX_LDPC_N {
                hdec[j] = if llr[j] >= 0.0 { 1 } else { 0 };
                if (hdec[j] ^ cw[j]) != 0 {
                    *dmin += llr[j].abs();
                }
            }
            *ntype = 2;
            return;
        }
    }

    *ntype = 0;
    *nharderror = -1;
    *dmin = 0.0;
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::common::ldpc::fast_atanh;

    #[test]
    fn ldpc_check_all_zeros() {
        let cw = [0u8; FTX_LDPC_N];
        assert_eq!(ldpc_check(&cw), 0);
    }

    #[test]
    fn ldpc_check_single_bit_error() {
        let mut cw = [0u8; FTX_LDPC_N];
        cw[0] = 1;
        assert!(ldpc_check(&cw) > 0);
    }

    #[test]
    fn fast_atanh_zero() {
        assert!(fast_atanh(0.0).abs() < 1e-6);
    }

    #[test]
    fn fast_atanh_approximation() {
        for &x in &[-0.5f32, -0.25, 0.25, 0.5] {
            let approx = fast_atanh(x);
            let exact = x.atanh();
            assert!(
                (approx - exact).abs() < 0.05,
                "fast_atanh({}) = {}, expected ~{}",
                x,
                approx,
                exact
            );
        }
    }

    #[test]
    fn platanh_small() {
        let result = platanh(0.5);
        assert!(result > 0.0);
        assert!(result.is_finite());
    }

    #[test]
    fn platanh_large() {
        let result = platanh(0.9999);
        assert!(result > 0.0);
        assert!(result.is_finite());
    }

    #[test]
    fn platanh_negative() {
        let pos = platanh(0.5);
        let neg = platanh(-0.5);
        assert!((pos + neg).abs() < 1e-6, "platanh should be odd");
    }

    #[test]
    fn shared_pack_bits_basic() {
        let mut bits = [0u8; FTX_LDPC_K];
        bits[0] = 1;
        bits[7] = 1;
        let mut packed = [0u8; FTX_LDPC_K_BYTES];
        pack_bits(&bits, FTX_LDPC_K, &mut packed);
        assert_eq!(packed[0], 0x81);
    }

    #[test]
    fn check_crc91_all_zeros() {
        // All-zero message likely fails CRC
        let bits = [0u8; FTX_LDPC_K];
        // CRC check result depends on specific polynomial behavior
        let _result = check_crc91(&bits);
        // Just verify it doesn't panic
    }

    #[test]
    fn shared_parity8_basic() {
        assert_eq!(parity8(0x00), 0);
        assert_eq!(parity8(0x01), 1);
        assert_eq!(parity8(0x03), 0);
        assert_eq!(parity8(0xFF), 0);
    }

    #[test]
    fn pattern_hash_basic() {
        let e2 = [1u8, 0, 1, 0];
        assert_eq!(pattern_hash(&e2, 4), 0b1010);
    }

    #[test]
    fn pattern_hash_all_zeros() {
        let e2 = [0u8; 16];
        assert_eq!(pattern_hash(&e2, 16), 0);
    }

    #[test]
    fn nextpat91_basic() {
        let k = 5;
        let mut mi = vec![0u8; k];
        mi[4] = 1;
        let mut iflag = 4i32;
        nextpat91(&mut mi, k, 1, &mut iflag);
        // After one step, the pattern should shift
        assert!(iflag >= -1);
    }

    #[test]
    fn generator_matrix_row_zero() {
        let gen = generator_matrix();
        // Row 0 should encode unit vector e_0
        assert_eq!(gen[0][0], 1);
        // Some parity bits should be non-zero
        let parity_nonzero = gen[0][FTX_LDPC_K..FTX_LDPC_N].iter().any(|&b| b != 0);
        assert!(parity_nonzero);
    }

    #[test]
    fn encode174_91_nocrc_all_zeros() {
        let msg = [0u8; FTX_LDPC_K];
        let mut cw = [0u8; FTX_LDPC_N];
        encode174_91_nocrc_bits(&msg, &mut cw);
        for &b in &cw {
            assert_eq!(b, 0);
        }
    }

    #[test]
    fn osd_box_basic() {
        let mut b = OsdBox::new(4).unwrap();
        let pattern = [1u8, 0, 1, 0];
        b.boxit(&pattern, 4, 5, 3);
        let (i1, i2) = b.fetchit(&pattern, 4);
        assert_eq!(i1, 5);
        assert_eq!(i2, 3);
    }

    #[test]
    fn osd_box_empty_fetch() {
        let mut b = OsdBox::new(4).unwrap();
        let pattern = [0u8; 4];
        let (i1, i2) = b.fetchit(&pattern, 4);
        assert_eq!(i1, -1);
        assert_eq!(i2, -1);
    }
}
