use bitcoincash::consensus::encode::ReadExt;
use bitcoincash::consensus::Decodable;
use bitcoincash::consensus::Encodable;
use bitcoincash::consensus::WriteExt;
use std::io::{self};

/**
 * What is implemented as `VarInt` in bitcoind.
 *
 * The `VarInt` in `bitcoincash` crate is CompactSize in `bitcoind`.
 */
#[derive(Copy, PartialEq, Eq, Clone, Debug, Default)]
pub struct BitcoindVarInt(pub u64);

impl Decodable for BitcoindVarInt {
    #[inline]
    fn consensus_decode<R: io::Read + ?Sized>(
        r: &mut R,
    ) -> Result<Self, bitcoincash::consensus::encode::Error> {
        let mut v: u64 = 0;
        loop {
            let x = ReadExt::read_u8(r)?;
            v = (v << 7) | (x & 0x7f) as u64;
            if x >= 0x80 {
                v += 1;
                continue;
            }
            return Ok(BitcoindVarInt(v));
        }
    }
}

impl Encodable for BitcoindVarInt {
    #[inline]
    fn consensus_encode<W: std::io::Write + ?Sized>(
        &self,
        w: &mut W,
    ) -> Result<usize, std::io::Error> {
        // TODO: Can we do size_of::<S>() in future Rust?
        const TMP_SIZE: usize = (u64::BITS * 8).div_ceil(7) as usize;
        let mut tmp: [u8; TMP_SIZE] = [0; TMP_SIZE];
        let mut n = self.0;
        let mut len = 0;
        loop {
            tmp[len] = (n & 0x7F) as u8 | (if len != 0 { 0x80 } else { 0x00 });
            if n <= 0x7F {
                break;
            }
            n = (n >> 7) - 1;
            len += 1;
        }
        let encoded_size = len + 1;
        loop {
            w.emit_u8(tmp[len])?;
            if len == 0 {
                break;
            }
            len -= 1;
        }
        Ok(encoded_size)
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use bitcoincash::consensus::deserialize;
    use bitcoincash::consensus::serialize;

    #[test]
    fn encode_decode() {
        let test = |number, expected_encoding_length| {
            let n = BitcoindVarInt(number);
            let s = serialize(&n);
            let u: BitcoindVarInt = deserialize(&s).unwrap();
            assert_eq!(s.len(), expected_encoding_length, "Encoded length mismatch");
            assert_eq!(u.0, number, "Encoded/decoded does not match original");
        };

        test(0, 1);
        test(127, 1);
        test(128, 2);
    }
}
