//! Git pkt-line protocol codec
//!
//! The pkt-line format is a simple length-prefixed line format used by git
//! for communication. Each packet starts with a 4-digit hex length (including
//! the 4 bytes themselves), followed by the payload. The special length "0000"
//! is a flush packet.

use std::io::{self, Read, Write};

/// A flush packet (length 0000)
pub const FLUSH: &[u8] = b"0000";

/// Encodes a pkt-line with the given payload.
///
/// The length includes the 4-byte length prefix itself.
pub fn encode(payload: &[u8], out: &mut impl Write) -> io::Result<()> {
    let len = payload.len() + 4;
    if len > 0xffff {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            "pkt-line payload too large",
        ));
    }
    write!(out, "{:04x}", len)?;
    out.write_all(payload)?;
    Ok(())
}

/// Encodes a flush packet.
pub fn encode_flush(out: &mut impl Write) -> io::Result<()> {
    out.write_all(FLUSH)
}

/// Encodes a pkt-line with a newline-terminated payload.
///
/// Convenience function that adds a newline to the payload before encoding.
pub fn encode_line(line: &str, out: &mut impl Write) -> io::Result<()> {
    let mut payload = line.as_bytes().to_vec();
    if !payload.ends_with(b"\n") {
        payload.push(b'\n');
    }
    encode(&payload, out)
}

/// A pkt-line packet
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Packet {
    /// A flush packet
    Flush,
    /// A data packet with the payload
    Data(Vec<u8>),
}

impl Packet {
    /// Returns true if this is a flush packet.
    pub fn is_flush(&self) -> bool {
        matches!(self, Packet::Flush)
    }

    /// Returns the payload if this is a data packet.
    pub fn data(&self) -> Option<&[u8]> {
        match self {
            Packet::Flush => None,
            Packet::Data(data) => Some(data),
        }
    }
}

/// Decodes a single pkt-line from the input.
///
/// Returns None if the input is exhausted (EOF).
pub fn decode(in_stream: &mut impl Read) -> io::Result<Option<Packet>> {
    let mut len_buf = [0u8; 4];
    match in_stream.read_exact(&mut len_buf) {
        Ok(_) => {}
        Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
        Err(e) => return Err(e),
    }

    let len_str = std::str::from_utf8(&len_buf).map_err(|_| {
        io::Error::new(
            io::ErrorKind::InvalidData,
            "pkt-line length not valid UTF-8",
        )
    })?;

    let len = usize::from_str_radix(len_str, 16)
        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "pkt-line length not valid hex"))?;

    if len == 0 {
        return Ok(Some(Packet::Flush));
    }

    if len < 4 {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "pkt-line length too small",
        ));
    }

    let payload_len = len - 4;
    let mut payload = vec![0u8; payload_len];
    in_stream.read_exact(&mut payload)?;

    Ok(Some(Packet::Data(payload)))
}

/// Reads a single text line (newline-terminated) from the input.
///
/// Used by the proc-receive hook to read the initial capability line,
/// which git sends as a plain line rather than a pkt-line.
pub fn read_line(in_stream: &mut impl Read) -> io::Result<String> {
    let mut line = String::new();
    let mut buf = [0u8; 1];
    loop {
        match in_stream.read_exact(&mut buf) {
            Ok(_) => {
                if buf[0] == b'\n' {
                    return Ok(line);
                }
                line.push(buf[0] as char);
            }
            Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
                return Ok(line);
            }
            Err(e) => return Err(e),
        }
    }
}

/// Writes a string followed by a newline.
pub fn write_line(line: &str, out: &mut impl Write) -> io::Result<()> {
    out.write_all(line.as_bytes())?;
    if !line.ends_with('\n') {
        out.write_all(b"\n")?;
    }
    Ok(())
}

/// Reads all packets from the input until a flush packet or EOF.
pub fn read_packets(in_stream: &mut impl Read) -> io::Result<Vec<Packet>> {
    let mut packets = Vec::new();
    loop {
        match decode(in_stream)? {
            None => break,
            Some(Packet::Flush) => {
                packets.push(Packet::Flush);
                break;
            }
            Some(packet) => packets.push(packet),
        }
    }
    Ok(packets)
}

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

    #[test]
    fn encode_empty_payload() {
        let mut buf = Vec::new();
        encode(b"", &mut buf).unwrap();
        assert_eq!(buf, b"0004");
    }

    #[test]
    fn encode_with_payload() {
        let mut buf = Vec::new();
        encode(b"hello", &mut buf).unwrap();
        assert_eq!(buf, b"0009hello");
    }

    #[test]
    fn test_encode_flush() {
        let mut buf = Vec::new();
        encode_flush(&mut buf).unwrap();
        assert_eq!(buf, b"0000");
    }

    #[test]
    fn encode_line_adds_newline() {
        let mut buf = Vec::new();
        encode_line("hello", &mut buf).unwrap();
        assert_eq!(buf, b"000ahello\n");
    }

    #[test]
    fn encode_line_keeps_existing_newline() {
        let mut buf = Vec::new();
        encode_line("hello\n", &mut buf).unwrap();
        assert_eq!(buf, b"000ahello\n");
    }

    #[test]
    fn decode_flush() {
        let mut cursor = std::io::Cursor::new(b"0000");
        let packet = decode(&mut cursor).unwrap().unwrap();
        assert_eq!(packet, Packet::Flush);
    }

    #[test]
    fn decode_with_payload() {
        let mut cursor = std::io::Cursor::new(b"0009hello");
        let packet = decode(&mut cursor).unwrap().unwrap();
        assert_eq!(packet, Packet::Data(b"hello".to_vec()));
    }

    #[test]
    fn decode_eof() {
        let mut cursor = std::io::Cursor::new(b"");
        assert!(decode(&mut cursor).unwrap().is_none());
    }

    #[test]
    fn round_trip() {
        let mut buf = Vec::new();
        encode(b"first", &mut buf).unwrap();
        encode(b"second", &mut buf).unwrap();
        encode_flush(&mut buf).unwrap();

        let mut cursor = std::io::Cursor::new(buf);
        let packets = read_packets(&mut cursor).unwrap();

        assert_eq!(packets.len(), 3);
        assert_eq!(packets[0], Packet::Data(b"first".to_vec()));
        assert_eq!(packets[1], Packet::Data(b"second".to_vec()));
        assert_eq!(packets[2], Packet::Flush);
    }
}
