use prost::Message;
use proto::PacketHead;
use thiserror::Error;
use tokio::io::{AsyncRead, AsyncReadExt};

const HEAD_MAGIC: u32 = 0x01234567;
const TAIL_MAGIC: u32 = 0x89ABCDEF;

pub struct NetPacket {
    pub cmd_id: u16,
    pub head: PacketHead,
    pub body: Box<[u8]>,
}

#[derive(Error, Debug)]
pub enum DecodeError {
    #[error("I/O error: {0}")]
    IoError(#[from] std::io::Error),
    #[error("head magic mismatch! expected: {HEAD_MAGIC}, got: {0}")]
    HeadMagicMismatch(u32),
    #[error("tail magic mismatch! expected: {TAIL_MAGIC}, got: {0}")]
    TailMagicMismatch(u32),
    #[error("failed to decode PacketHead: {0}")]
    PacketHeadDecodeError(#[from] prost::DecodeError),
}

impl NetPacket {
    pub async fn read<R>(r: &mut R) -> Result<Self, DecodeError>
    where
        R: AsyncRead + Send + Unpin,
    {
        let head_magic = r.read_u32().await?;
        matches!(head_magic, HEAD_MAGIC)
            .then_some(())
            .ok_or(DecodeError::HeadMagicMismatch(head_magic))?;

        let cmd_id = r.read_u16().await?;
        let head_len = r.read_u16().await? as usize;
        let body_len = r.read_u32().await? as usize;

        let mut head = vec![0u8; head_len];
        r.read_exact(&mut head).await?;

        let mut body = vec![0u8; body_len].into_boxed_slice();
        r.read_exact(&mut body).await?;

        let tail_magic = r.read_u32().await?;
        matches!(tail_magic, TAIL_MAGIC)
            .then_some(())
            .ok_or(DecodeError::TailMagicMismatch(tail_magic))?;

        let head = PacketHead::decode(&*head)?;

        Ok(Self { cmd_id, head, body })
    }

    pub fn encode(&self) -> Vec<u8> {
        let head = self.head.encode_to_vec();
        let mut out = Vec::with_capacity(16 + head.len() + self.body.len());

        out.extend(HEAD_MAGIC.to_be_bytes());
        out.extend(self.cmd_id.to_be_bytes());
        out.extend((head.len() as u16).to_be_bytes());
        out.extend((self.body.len() as u32).to_be_bytes());
        out.extend(head);
        out.extend(&self.body);
        out.extend(TAIL_MAGIC.to_be_bytes());

        out
    }
}
