use super::rs::{Gf, RsEncoder};
use super::{BarState, PostalVariant};
use crate::error::{Error, Result};
use crate::segment::{Mode, Segment};
const GDSET: &[u8; 64] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz #";
const N_TABLE: [[u8; 2]; 10] = [
[0, 0],
[0, 1],
[0, 2],
[1, 0],
[1, 1],
[1, 2],
[2, 0],
[2, 1],
[2, 2],
[3, 0],
];
const C_TABLE: [[u8; 3]; 64] = [
[2, 2, 2],
[3, 0, 0],
[3, 0, 1],
[3, 0, 2],
[3, 1, 0],
[3, 1, 1],
[3, 1, 2],
[3, 2, 0],
[3, 2, 1],
[3, 2, 2],
[0, 0, 0],
[0, 0, 1],
[0, 0, 2],
[0, 1, 0],
[0, 1, 1],
[0, 1, 2],
[0, 2, 0],
[0, 2, 1],
[0, 2, 2],
[1, 0, 0],
[1, 0, 1],
[1, 0, 2],
[1, 1, 0],
[1, 1, 1],
[1, 1, 2],
[1, 2, 0],
[1, 2, 1],
[1, 2, 2],
[2, 0, 0],
[2, 0, 1],
[2, 0, 2],
[2, 1, 0],
[2, 1, 1],
[2, 1, 2],
[2, 2, 0],
[2, 2, 1],
[0, 2, 3],
[0, 3, 0],
[0, 3, 1],
[0, 3, 2],
[0, 3, 3],
[1, 0, 3],
[1, 1, 3],
[1, 2, 3],
[1, 3, 0],
[1, 3, 1],
[1, 3, 2],
[1, 3, 3],
[2, 0, 3],
[2, 1, 3],
[2, 2, 3],
[2, 3, 0],
[2, 3, 1],
[2, 3, 2],
[2, 3, 3],
[3, 0, 3],
[3, 1, 3],
[3, 2, 3],
[3, 3, 0],
[3, 3, 1],
[3, 3, 2],
[3, 3, 3],
[0, 0, 3],
[0, 1, 3],
];
const FCCS: [[u8; 2]; 7] = [*b"00", *b"11", *b"59", *b"62", *b"45", *b"87", *b"92"];
const START_STOP: [u8; 2] = [1, 3];
fn value_to_bar(v: u8) -> BarState {
match v {
0 => BarState::Full,
1 => BarState::Ascender,
2 => BarState::Descender,
_ => BarState::Tracker,
}
}
fn bar_to_value(b: BarState) -> u8 {
match b {
BarState::Full => 0,
BarState::Ascender => 1,
BarState::Descender => 2,
BarState::Tracker => 3,
}
}
fn gdset_index(byte: u8) -> Option<usize> {
GDSET.iter().position(|&c| c == byte)
}
fn rs_check(triples: &[u8]) -> Vec<u8> {
let gf = Gf::new(0x43, 6);
RsEncoder::new(&gf, 4, 1).encode(triples)
}
struct Layout {
fcc_idx: usize,
c_table: bool,
filler_target: usize,
}
fn plan(dpid: &[u8], custinfo: &[u8]) -> Result<Layout> {
if dpid.len() != 8 || !dpid.iter().all(u8::is_ascii_digit) {
return Err(Error::invalid_data("Australia Post DPID must be 8 digits"));
}
let length = 8 + custinfo.len();
if length > 23 {
return Err(Error::capacity(
"Australia Post input too long (maximum 23)",
));
}
if custinfo.iter().any(|&b| gdset_index(b).is_none()) {
return Err(Error::invalid_data(
"Australia Post customer information must be graphic-set (0-9 A-Z a-z space #)",
));
}
let c_table = custinfo.iter().any(|&b| !b.is_ascii_digit());
let (fcc_idx, filler_target) = if custinfo.is_empty() {
(1, 23) } else if length > 18 {
if c_table {
return Err(Error::capacity(
"Australia Post customer information over 10 characters must be digits only",
));
}
(3, 53) } else if length > 16 || (length > 13 && c_table) {
(3, 53)
} else {
(2, 38) };
let fcc_idx = if dpid.iter().all(|&b| b == b'0') {
0
} else {
fcc_idx
};
Ok(Layout {
fcc_idx,
c_table,
filler_target,
})
}
pub(super) fn encode(dpid: &[u8], custinfo: &[u8]) -> Result<Vec<BarState>> {
let layout = plan(dpid, custinfo)?;
let mut dest: Vec<u8> = Vec::new();
dest.extend_from_slice(&START_STOP);
for &d in &FCCS[layout.fcc_idx] {
dest.extend_from_slice(&N_TABLE[(d - b'0') as usize]);
}
for &d in dpid {
dest.extend_from_slice(&N_TABLE[(d - b'0') as usize]);
}
if layout.c_table {
for &c in custinfo {
dest.extend_from_slice(&C_TABLE[gdset_index(c).unwrap()]);
}
} else {
for &d in custinfo {
dest.extend_from_slice(&N_TABLE[(d - b'0') as usize]);
}
}
while dest.len() != layout.filler_target {
dest.push(3);
}
let triples: Vec<u8> = dest[2..]
.chunks_exact(3)
.map(|c| (c[0] << 4) | (c[1] << 2) | c[2])
.collect();
for &sym in &rs_check(&triples) {
dest.push((sym >> 4) & 3);
dest.push((sym >> 2) & 3);
dest.push(sym & 3);
}
dest.extend_from_slice(&START_STOP);
Ok(dest.iter().map(|&v| value_to_bar(v)).collect())
}
fn format_ok(n: usize) -> bool {
matches!(n, 37 | 52 | 67)
}
pub(super) fn decode(bars: &[BarState]) -> Result<(PostalVariant, Vec<Segment>)> {
let n = bars.len();
if !format_ok(n) {
return Err(Error::undecodable("Australia Post bar count invalid"));
}
let v: Vec<u8> = bars.iter().map(|&b| bar_to_value(b)).collect();
if v[..2] != START_STOP || v[n - 2..] != START_STOP {
return Err(Error::undecodable("Australia Post start/stop bars invalid"));
}
let data_region = &v[2..n - 14]; let rs_region = &v[n - 14..n - 2];
if !data_region.len().is_multiple_of(3) {
return Err(Error::undecodable("Australia Post data region misaligned"));
}
let triples: Vec<u8> = data_region
.chunks_exact(3)
.map(|c| (c[0] << 4) | (c[1] << 2) | c[2])
.collect();
let expected = rs_check(&triples);
let got: Vec<u8> = rs_region
.chunks_exact(3)
.map(|c| (c[0] << 4) | (c[1] << 2) | c[2])
.collect();
if expected != got {
return Err(Error::undecodable("Australia Post Reed-Solomon mismatch"));
}
for i in 0..2 {
let pair = [data_region[i * 2], data_region[i * 2 + 1]];
if !N_TABLE.contains(&pair) {
return Err(Error::undecodable("Australia Post FCC invalid"));
}
}
let mut dpid = Vec::with_capacity(8);
for i in 2..10 {
let pair = [data_region[i * 2], data_region[i * 2 + 1]];
let d = N_TABLE
.iter()
.position(|&e| e == pair)
.ok_or_else(|| Error::undecodable("Australia Post DPID invalid"))?;
dpid.push(b'0' + d as u8);
}
let mid = &data_region[20..];
let custinfo = decode_custinfo(mid);
let mut segments = vec![Segment::numeric(dpid)];
match custinfo {
CustInfo::None => {}
CustInfo::Numeric(bytes) => segments.push(Segment::numeric(bytes)),
CustInfo::Bytes(bytes) => segments.push(Segment::byte(bytes)),
}
Ok((PostalVariant::AustraliaPost, segments))
}
enum CustInfo {
None,
Numeric(Vec<u8>),
Bytes(Vec<u8>),
}
fn decode_custinfo(mid: &[u8]) -> CustInfo {
if mid.iter().all(|&x| x == 3) {
return CustInfo::None;
}
if let Some(digits) = parse_n(mid) {
return CustInfo::Numeric(digits);
}
if let Some(bytes) = parse_c(mid) {
return CustInfo::Bytes(bytes);
}
CustInfo::None
}
fn parse_n(mid: &[u8]) -> Option<Vec<u8>> {
let mut end = mid.len();
while end > 0 && mid[end - 1] == 3 {
end -= 1;
}
let core = &mid[..end];
if !core.len().is_multiple_of(2) {
return None;
}
let mut out = Vec::with_capacity(core.len() / 2);
for pair in core.chunks_exact(2) {
let d = N_TABLE.iter().position(|&e| e == [pair[0], pair[1]])? as u8;
out.push(b'0' + d);
}
Some(out)
}
fn parse_c(mid: &[u8]) -> Option<Vec<u8>> {
let max_chars = mid.len() / 3;
for chars in 0..=max_chars {
let consumed = chars * 3;
if mid[consumed..].iter().any(|&x| x != 3) {
continue;
}
let mut out = Vec::with_capacity(chars);
let mut ok = true;
for tri in mid[..consumed].chunks_exact(3) {
match C_TABLE.iter().position(|&e| e == [tri[0], tri[1], tri[2]]) {
Some(i) => out.push(GDSET[i]),
None => {
ok = false;
break;
}
}
}
if ok {
return Some(out);
}
}
None
}
pub(super) fn fields(segments: &[Segment]) -> Result<(Vec<u8>, Vec<u8>)> {
let dpid = segments
.first()
.filter(|s| matches!(s.mode, Mode::Numeric))
.map(|s| s.data.clone())
.ok_or_else(|| Error::invalid_data("Australia Post symbol missing DPID segment"))?;
let custinfo = segments.get(1).map(|s| s.data.clone()).unwrap_or_default();
Ok((dpid, custinfo))
}