use super::ap::ApHint;
use super::wsjt77;
use super::{CallsignHashTable, Wsjt77Message};
use crate::core::{DecodeContext, MessageCodec, MessageFields};
pub fn pack77_to_symbols(bits77: &[u8; 77]) -> [i32; 13] {
let mut out = [0_i32; 13];
for (i, slot) in out.iter_mut().enumerate().take(12) {
let mut s = 0_i32;
for b in 0..6 {
s = (s << 1) | (bits77[6 * i + b] & 1) as i32;
}
*slot = s;
}
let mut last = 0_i32;
for b in 0..5 {
last = (last << 1) | (bits77[72 + b] & 1) as i32;
}
out[12] = last << 1;
out
}
pub fn ap_hint_to_q65_mask(hint: &ApHint) -> ([i32; 13], [i32; 13]) {
let (mut mask77, mut values77) = hint.build_bits(77);
let lock_padding = if hint.has_info() { 1 } else { 0 };
mask77.push(lock_padding);
values77.push(0);
let mut mask_syms = [0_i32; 13];
let mut value_syms = [0_i32; 13];
for i in 0..13 {
let mut m = 0_i32;
let mut v = 0_i32;
for b in 0..6 {
m = (m << 1) | (mask77[6 * i + b] & 1) as i32;
v = (v << 1) | (values77[6 * i + b] & 1) as i32;
}
mask_syms[i] = m;
value_syms[i] = v;
}
(mask_syms, value_syms)
}
pub fn unpack_symbols_to_bits77(symbols: &[i32; 13]) -> [u8; 77] {
let mut bits = [0_u8; 77];
for i in 0..12 {
let s = symbols[i];
for b in 0..6 {
bits[6 * i + b] = ((s >> (5 - b)) & 1) as u8;
}
}
let s = symbols[12];
for b in 0..5 {
bits[72 + b] = ((s >> (5 - b)) & 1) as u8;
}
bits
}
#[derive(Copy, Clone, Debug, Default)]
pub struct Q65Message;
impl MessageCodec for Q65Message {
type Unpacked = String;
const PAYLOAD_BITS: u32 = 77;
const CRC_BITS: u32 = 12;
fn pack(&self, fields: &MessageFields) -> Option<Vec<u8>> {
Wsjt77Message.pack(fields)
}
fn unpack(&self, payload: &[u8], ctx: &DecodeContext) -> Option<Self::Unpacked> {
if payload.len() != 77 {
return None;
}
let mut buf = [0u8; 77];
buf.copy_from_slice(payload);
if let Some(any) = ctx.callsign_hash_table.as_ref()
&& let Some(ht) = any.downcast_ref::<CallsignHashTable>()
{
return wsjt77::unpack77_with_hash(&buf, ht);
}
wsjt77::unpack77(&buf)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pack_unpack_roundtrip_random_bits() {
let cases: Vec<[u8; 77]> = vec![
[0u8; 77],
[1u8; 77],
std::array::from_fn(|i| (((i * 31) ^ 0x55) & 1) as u8),
];
for bits in cases {
let symbols = pack77_to_symbols(&bits);
for (k, s) in symbols.iter().enumerate() {
assert!(*s >= 0 && *s < 64, "symbol[{k}] = {s} out of range");
}
let back = unpack_symbols_to_bits77(&symbols);
assert_eq!(back, bits, "77-bit roundtrip failed");
}
}
#[test]
fn last_symbol_has_zero_lsb_after_pack() {
let mut bits = [0u8; 77];
for b in 72..77 {
bits[b] = 1;
}
let symbols = pack77_to_symbols(&bits);
assert_eq!(symbols[12], 62);
assert_eq!(symbols[12] & 1, 0, "LSB padding bit must be 0");
}
#[test]
fn message_codec_pack_matches_wsjt77() {
let fields = MessageFields {
call1: Some("CQ".to_string()),
call2: Some("JA1ABC".to_string()),
grid: Some("PM95".to_string()),
..Default::default()
};
let q65 = Q65Message.pack(&fields).expect("Q65 pack must succeed");
let wsjt = Wsjt77Message
.pack(&fields)
.expect("Wsjt77 pack must succeed");
assert_eq!(q65, wsjt);
assert_eq!(q65.len(), 77);
}
#[test]
fn unpack_roundtrip_preserves_message_text() {
let fields = MessageFields {
call1: Some("CQ".to_string()),
call2: Some("K1ABC".to_string()),
grid: Some("FN42".to_string()),
..Default::default()
};
let bits = Q65Message.pack(&fields).expect("pack");
let bits77: [u8; 77] = bits.try_into().expect("77-bit length");
let symbols = pack77_to_symbols(&bits77);
let back = unpack_symbols_to_bits77(&symbols);
let text = Q65Message
.unpack(&back, &DecodeContext::default())
.expect("unpack");
assert_eq!(text, "CQ K1ABC FN42");
}
#[test]
fn payload_and_crc_bit_widths() {
assert_eq!(<Q65Message as MessageCodec>::PAYLOAD_BITS, 77);
assert_eq!(<Q65Message as MessageCodec>::CRC_BITS, 12);
}
#[test]
fn ap_hint_empty_yields_no_locked_symbols() {
let hint = ApHint::new();
let (mask, values) = ap_hint_to_q65_mask(&hint);
assert_eq!(mask, [0; 13], "empty hint must mask nothing");
assert_eq!(values, [0; 13], "empty hint values irrelevant but zeroed");
}
#[test]
fn ap_hint_with_call1_locks_first_29_bits() {
let hint = ApHint::new().with_call1("CQ");
let (mask, _) = ap_hint_to_q65_mask(&hint);
assert_eq!(mask[0], 0x3F, "sym 0 must be fully locked (bits 0..6)");
assert_eq!(mask[1], 0x3F, "sym 1 must be fully locked (bits 6..12)");
assert_eq!(mask[2], 0x3F, "sym 2 must be fully locked (bits 12..18)");
assert_eq!(mask[3], 0x3F, "sym 3 must be fully locked (bits 18..24)");
assert_eq!(mask[4], 0b111110, "sym 4 must lock its top 5 bits");
assert_eq!(mask[5], 0, "sym 5 (bits 30..36) untouched without call2");
}
#[test]
fn ap_hint_padding_bit_is_locked_when_info_present() {
let hint = ApHint::new().with_call1("CQ");
let (mask, values) = ap_hint_to_q65_mask(&hint);
assert_eq!(mask[12] & 1, 1, "sym 12 LSB (= padding bit) must be locked");
assert_eq!(values[12] & 1, 0, "padding bit value must be 0");
}
#[test]
fn ap_hint_round_trip_preserves_known_bits() {
let fields = MessageFields {
call1: Some("CQ".to_string()),
call2: Some("K1ABC".to_string()),
grid: Some("FN42".to_string()),
..Default::default()
};
let bits77 = Q65Message.pack(&fields).expect("pack");
let bits77_arr: [u8; 77] = bits77.try_into().expect("77-bit length");
let encoded_syms = pack77_to_symbols(&bits77_arr);
let hint = ApHint::new()
.with_call1("CQ")
.with_call2("K1ABC")
.with_grid("FN42");
let (mask, values) = ap_hint_to_q65_mask(&hint);
for k in 0..13 {
let m = mask[k];
let v = values[k];
let actual = encoded_syms[k];
assert_eq!(
v & m,
actual & m,
"sym {k}: AP value {v:06b} mismatches encoded {actual:06b} under mask {m:06b}"
);
}
}
}