//! Heat2 varint.
//!
//! The first byte is the odd one: only six data bits (`0x3F`), with `0x40` as a
//! sign flag and `0x80` as "more". Every later byte is a conventional 7-bit
//! group. Groups are little-endian: byte 0 holds the low six bits, then seven
//! bits at shifts 6, 13, 20, ...
//!
//! Evidence: validated byte-exact against the FIFA 17 preAuth capture
//! (`LANG = 0x656E5553` encodes as `93 d5 f2 d6 0c`).
//!
//! The sign flag is UNVERIFIED — it never appears in any captured frame. It is
//! implemented to match the oracle so negatives round-trip, but no claim is
//! made that EA encodes negatives this way.

use crate::error::{Error, Result};

/// Append the canonical (shortest) encoding of `value`.
pub fn encode(value: i64, out: &mut Vec<u8>) {
    let neg = value < 0;
    // unsigned_abs, not -value: i64::MIN has no positive counterpart.
    let mut v = value.unsigned_abs();

    let mut first = (v & 0x3F) as u8;
    v >>= 6;
    if neg {
        first |= 0x40;
    }
    if v == 0 {
        out.push(first);
        return;
    }
    out.push(first | 0x80);
    while v >= 0x80 {
        out.push(((v & 0x7F) as u8) | 0x80);
        v >>= 7;
    }
    out.push(v as u8);
}

/// Decode at `pos`, returning the value and the index just past it.
pub fn decode(buf: &[u8], pos: usize) -> Result<(i64, usize)> {
    let mut i = pos;
    let b = *buf.get(i).ok_or(Error::Truncated {
        what: "varint",
        need: 1,
        have: 0,
    })?;
    i += 1;

    let mut val: u64 = (b & 0x3F) as u64;
    let neg = b & 0x40 != 0;

    if b & 0x80 != 0 {
        let mut shift = 6u32;
        loop {
            let b = *buf.get(i).ok_or(Error::Truncated {
                what: "varint continuation",
                need: 1,
                have: 0,
            })?;
            i += 1;
            if shift >= 64 {
                return Err(Error::VarintOverflow { at: pos });
            }
            val |= ((b & 0x7F) as u64) << shift;
            shift += 7;
            if b & 0x80 == 0 {
                break;
            }
        }
    }

    let signed = if neg {
        (val as i64).wrapping_neg()
    } else {
        val as i64
    };
    Ok((signed, i))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn enc(v: i64) -> Vec<u8> {
        let mut o = Vec::new();
        encode(v, &mut o);
        o
    }

    #[test]
    fn single_byte_below_0x40() {
        assert_eq!(enc(0), vec![0x00]);
        assert_eq!(enc(1), vec![0x01]);
        assert_eq!(enc(0x3F), vec![0x3F]);
    }

    #[test]
    fn spills_to_a_second_group_at_0x40() {
        // Six data bits in byte 0, so 0x40 is the first value that needs two.
        assert_eq!(enc(0x40), vec![0x80, 0x01]);
    }

    #[test]
    fn matches_the_captured_lang_field() {
        assert_eq!(enc(0x656E5553), vec![0x93, 0xd5, 0xf2, 0xd6, 0x0c]);
    }

    #[test]
    fn round_trips_boundaries() {
        for v in [
            0,
            1,
            0x3F,
            0x40,
            0x7F,
            0x80,
            0x1FFF,
            0x2000,
            0xFFFF_FFFF,
            i64::MAX,
            -1,
            -300,
        ] {
            let bytes = enc(v);
            assert_eq!(decode(&bytes, 0).unwrap(), (v, bytes.len()), "value {v}");
        }
    }

    #[test]
    fn rejects_a_never_terminating_varint() {
        let runaway = vec![0xFF; 32];
        assert!(matches!(
            decode(&runaway, 0),
            Err(Error::VarintOverflow { .. })
        ));
    }

    #[test]
    fn rejects_truncation() {
        assert!(decode(&[0x80], 0).is_err());
        assert!(decode(&[], 0).is_err());
    }
}
