use std::fmt;

use crate::compression::{compress_lz4_block, decompress_payload, CompressionError};
use crate::frame::{decode, encode, Frame, FrameError};
use crate::packet::{command, Packet, PROTOCOL_VERSION};

const COMPRESSION_THRESHOLD: usize = 32;

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CodecError {
    Frame(FrameError),
    Compression(CompressionError),
}

impl fmt::Display for CodecError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Frame(error) => error.fmt(formatter),
            Self::Compression(error) => error.fmt(formatter),
        }
    }
}

impl std::error::Error for CodecError {}

impl From<FrameError> for CodecError {
    fn from(error: FrameError) -> Self {
        Self::Frame(error)
    }
}

impl From<CompressionError> for CodecError {
    fn from(error: CompressionError) -> Self {
        Self::Compression(error)
    }
}

pub fn encode_packet(
    command: u8,
    opcode: u16,
    sequence: u16,
    payload: &[u8],
) -> Result<Vec<u8>, CodecError> {
    let compressed = (payload.len() >= COMPRESSION_THRESHOLD).then(|| compress_lz4_block(payload));
    let (body, marker) = match compressed {
        Some(body) if body.len() < payload.len() => {
            let ratio = payload.len() / body.len().max(1);
            (
                body,
                u8::try_from(ratio.saturating_add(1)).unwrap_or(u8::MAX),
            )
        }
        _ => (payload.to_vec(), 0),
    };
    encode(&Frame {
        version: PROTOCOL_VERSION,
        command,
        sequence,
        opcode,
        compression: marker,
        payload: body,
    })
    .map_err(Into::into)
}

pub fn decode_packet(bytes: &[u8]) -> Result<Packet, CodecError> {
    decode_frame_packet(decode(bytes)?)
}

pub fn decode_frame_packet(frame: Frame) -> Result<Packet, CodecError> {
    let payload = if frame.compression == 0 {
        frame.payload
    } else {
        decompress_payload(&frame.payload)?
    };
    Ok(Packet {
        version: frame.version,
        command: frame.command,
        sequence: frame.sequence,
        opcode: frame.opcode,
        payload,
    })
}

pub fn encode_request(opcode: u16, sequence: u16, payload: &[u8]) -> Result<Vec<u8>, CodecError> {
    encode_packet(command::REQUEST, opcode, sequence, payload)
}

#[cfg(test)]
mod tests {
    use super::{decode_packet, encode_packet};
    use crate::packet::{command, Packet, PROTOCOL_VERSION};

    #[test]
    fn packet_round_trip_handles_compressible_payload() {
        let payload = vec![0x41; 4096];
        let wire = encode_packet(command::REQUEST, 64, 31, &payload).unwrap();
        assert!(wire[6] > 0);
        assert_eq!(
            decode_packet(&wire).unwrap(),
            Packet {
                version: PROTOCOL_VERSION,
                command: command::REQUEST,
                sequence: 31,
                opcode: 64,
                payload,
            }
        );
    }

    #[test]
    fn packet_round_trip_keeps_small_payload_uncompressed() {
        let wire = encode_packet(command::OK, 6, 2, &[0xaa, 0xbb]).unwrap();
        assert_eq!(wire[6], 0);
        assert_eq!(decode_packet(&wire).unwrap().payload, [0xaa, 0xbb]);
    }
}
