use super::frame::{CompressionTag, FrameBody, FrameHeader};
use super::limits::{validate_body_size, validate_ratio, validate_raw_size};
use super::message::{Compression, WireCodecError, WireLimits};

pub fn encode(
    raw: &[u8],
    policy: Compression,
    limits: WireLimits,
) -> Result<FrameBody, WireCodecError> {
    validate_raw_size(raw.len(), limits.max_raw)?;
    if !should_compress(raw.len(), policy, limits) {
        return raw_body(raw, limits);
    }
    let compressed = lz4_flex::block::compress(raw);
    choose_body(raw, compressed, limits)
}

pub fn decode(
    header: FrameHeader,
    body: &[u8],
    limits: WireLimits,
) -> Result<Vec<u8>, WireCodecError> {
    validate_body_size(body.len(), limits.max_body)?;
    validate_raw_size(header.raw_len(), limits.max_raw)?;
    if header.compression() == CompressionTag::Raw {
        validate_raw_body(header.raw_len(), body.len())?;
        return Ok(body.to_vec());
    }
    validate_ratio(header.raw_len(), body.len(), limits.max_ratio)?;
    lz4_flex::block::decompress(body, header.raw_len()).map_err(|error| {
        WireCodecError::new(
            format!("wire body failed LZ4 decompression: {error}"),
            "send bytes produced by efdn wire or drop the packet",
        )
    })
}

fn raw_body(raw: &[u8], limits: WireLimits) -> Result<FrameBody, WireCodecError> {
    validate_body_size(raw.len(), limits.max_body)?;
    Ok(FrameBody::new(raw.to_vec(), CompressionTag::Raw))
}

fn choose_body(
    raw: &[u8],
    compressed: Vec<u8>,
    limits: WireLimits,
) -> Result<FrameBody, WireCodecError> {
    if compression_wins(raw.len(), compressed.len(), limits) {
        validate_body_size(compressed.len(), limits.max_body)?;
        return Ok(FrameBody::new(compressed, CompressionTag::Lz4));
    }
    raw_body(raw, limits)
}

fn should_compress(size: usize, policy: Compression, limits: WireLimits) -> bool {
    match policy {
        Compression::Off => false,
        Compression::On => true,
        Compression::Auto { min_bytes } => {
            size as u64 > limits.max_body.get() || size as u64 >= min_bytes.get()
        }
    }
}

fn compression_wins(raw: usize, compressed: usize, limits: WireLimits) -> bool {
    compressed < raw && raw.saturating_sub(compressed) as u64 >= limits.min_saved.get()
}

fn validate_raw_body(raw: usize, body: usize) -> Result<(), WireCodecError> {
    if raw == body {
        return Ok(());
    }
    Err(WireCodecError::new(
        format!("raw wire body has {body} bytes, header declares {raw}"),
        "drop the frame or send bytes produced by efdn wire",
    ))
}
