use std::io::Write;

use flate2::Compression;
use flate2::write::DeflateEncoder;

use crate::dat::{
    BLOCK_HEADER_SIZE, BLOCK_INFO_SIZE, BLOCK_TYPE_UNCOMPRESSED, BlockHeader, BlockInfo, FileInfo,
    FileKind,
};
use crate::error::{Error, Result};
use crate::write::APPEND_BLOCK_SIZE;

/// Alignment (in bytes) applied to each compressed block within the `.dat` file.
pub(crate) const BLOCK_ALIGNMENT: u32 = 128;

/// Byte size of the [`FileInfo`](crate::dat::FileInfo) prefix that precedes the
/// [`BlockInfo`](crate::dat::BlockInfo) table in the on-disk payload header.
pub(crate) const FILE_INFO_LOGICAL_SIZE: u32 = 24;

fn align_up(n: u32) -> u32 {
    let rem = n % BLOCK_ALIGNMENT;
    if rem == 0 {
        n
    } else {
        n + (BLOCK_ALIGNMENT - rem)
    }
}

/// A fully serialized `SqPack` standard-file payload ready to be written to a `.dat` file.
///
/// Contains the [`FileInfo`](crate::dat::FileInfo) header, block-info table, and all
/// DEFLATE-compressed, 128-byte-aligned data blocks concatenated in order.
#[derive(Debug)]
pub(crate) struct EncodedPayload {
    /// Serialized bytes to append directly to a `.dat` file.
    pub bytes: Vec<u8>,
    /// Number of DEFLATE blocks emitted; stored in the `FileInfo` header.
    pub block_count: u32,
}

/// Encodes a raw file payload as a `SqPack` standard-file entry.
///
/// Splits `payload` into [`APPEND_BLOCK_SIZE`](crate::write::APPEND_BLOCK_SIZE)-byte chunks,
/// DEFLATE-compresses each chunk, pads every compressed block to a 128-byte boundary, and
/// assembles the complete on-disk representation including the [`FileInfo`](crate::dat::FileInfo)
/// prefix and per-block [`BlockInfo`](crate::dat::BlockInfo) table.
///
/// An empty `payload` produces a single zero-length block so the file header is valid.
#[allow(clippy::too_many_lines)]
pub(crate) fn encode_standard(payload: &[u8]) -> Result<EncodedPayload> {
    let raw_file_size = u32::try_from(payload.len()).map_err(|_| Error::AppendFileTooLarge {
        size: payload.len() as u64,
        max: u64::from(u32::MAX),
    })?;

    let chunk_size = APPEND_BLOCK_SIZE as usize;
    let block_count_usize = if payload.is_empty() {
        1
    } else {
        payload.len().div_ceil(chunk_size)
    };
    let block_count = u32::try_from(block_count_usize).map_err(|_| Error::InvalidBlockLayout {
        context: "append.block_count",
        message: format!("block count {block_count_usize} exceeds u32::MAX"),
    })?;

    let info_size = FILE_INFO_LOGICAL_SIZE
        .checked_add(BLOCK_INFO_SIZE.checked_mul(block_count).ok_or_else(|| {
            Error::InvalidBlockLayout {
                context: "append.block_info_size",
                message: format!("block_count {block_count} × BLOCK_INFO_SIZE overflows u32"),
            }
        })?)
        .ok_or_else(|| Error::InvalidBlockLayout {
            context: "append.info_size",
            message: format!("info_size for block_count {block_count} overflows u32"),
        })?;

    let mut compressed_blocks: Vec<Vec<u8>> = Vec::with_capacity(block_count_usize);
    let mut block_infos: Vec<BlockInfo> = Vec::with_capacity(block_count_usize);
    let mut cursor: u32 = 0;

    let chunks: Box<dyn Iterator<Item = &[u8]>> = if payload.is_empty() {
        Box::new(std::iter::once(&[][..]))
    } else {
        Box::new(payload.chunks(chunk_size))
    };

    for chunk in chunks {
        let uncompressed_size_u32 =
            u32::try_from(chunk.len()).map_err(|_| Error::InvalidBlockLayout {
                context: "append.chunk_size",
                message: format!("chunk size {} exceeds u32::MAX", chunk.len()),
            })?;
        let mut enc = DeflateEncoder::new(Vec::new(), Compression::default());
        enc.write_all(chunk)?;
        let compressed = enc.finish()?;
        let compressed_size_u16 =
            u16::try_from(compressed.len()).map_err(|_| Error::InvalidBlockLayout {
                context: "append.compressed_size",
                message: format!(
                    "compressed block size {} exceeds u16::MAX",
                    compressed.len()
                ),
            })?;
        let uncompressed_size_u16 =
            u16::try_from(chunk.len()).map_err(|_| Error::InvalidBlockLayout {
                context: "append.uncompressed_size",
                message: format!("uncompressed chunk size {} exceeds u16::MAX", chunk.len()),
            })?;

        let on_disk_unpadded = BLOCK_HEADER_SIZE
            .checked_add(u32::from(compressed_size_u16))
            .ok_or_else(|| Error::InvalidBlockLayout {
                context: "append.block_on_disk",
                message: format!(
                    "BLOCK_HEADER_SIZE + compressed_size ({compressed_size_u16}) overflows u32"
                ),
            })?;
        let on_disk_padded = align_up(on_disk_unpadded);
        let padded_u16 = u16::try_from(on_disk_padded).map_err(|_| Error::InvalidBlockLayout {
            context: "append.block_on_disk_u16",
            message: format!("padded block size {on_disk_padded} exceeds u16::MAX"),
        })?;

        block_infos.push(BlockInfo {
            offset: cursor,
            compressed_size: padded_u16,
            uncompressed_size: uncompressed_size_u16,
        });

        let mut block_bytes = Vec::with_capacity(on_disk_padded as usize);
        let bh_dat_block_type: u32 = 0;
        let bh = BlockHeader {
            size: BLOCK_HEADER_SIZE,
            unknown1: 0,
            dat_block_type: bh_dat_block_type,
            block_data_size: uncompressed_size_u32,
        };
        block_bytes.extend_from_slice(&bh.size.to_le_bytes());
        block_bytes.extend_from_slice(&bh.unknown1.to_le_bytes());
        block_bytes.extend_from_slice(&bh.dat_block_type.to_le_bytes());
        block_bytes.extend_from_slice(&bh.block_data_size.to_le_bytes());
        block_bytes.extend_from_slice(&compressed);
        block_bytes.resize(on_disk_padded as usize, 0);
        compressed_blocks.push(block_bytes);

        cursor = cursor
            .checked_add(on_disk_padded)
            .ok_or_else(|| Error::InvalidBlockLayout {
                context: "append.cursor",
                message: format!("block-region cursor + {on_disk_padded} overflows u32"),
            })?;
    }

    let total_size = u64::from(info_size) + u64::from(cursor);
    let mut bytes = Vec::with_capacity(usize::try_from(total_size).unwrap_or(usize::MAX));

    let file_info = FileInfo {
        size: info_size,
        kind_raw: FileKind::Standard.as_raw(),
        raw_file_size,
        unknown: [0, 0],
        number_of_blocks: block_count,
    };
    bytes.extend_from_slice(&file_info.size.to_le_bytes());
    bytes.extend_from_slice(&file_info.kind_raw.to_le_bytes());
    bytes.extend_from_slice(&file_info.raw_file_size.to_le_bytes());
    bytes.extend_from_slice(&file_info.unknown[0].to_le_bytes());
    bytes.extend_from_slice(&file_info.unknown[1].to_le_bytes());
    bytes.extend_from_slice(&file_info.number_of_blocks.to_le_bytes());
    for bi in &block_infos {
        bytes.extend_from_slice(&bi.offset.to_le_bytes());
        bytes.extend_from_slice(&bi.compressed_size.to_le_bytes());
        bytes.extend_from_slice(&bi.uncompressed_size.to_le_bytes());
    }
    for block in &compressed_blocks {
        bytes.extend_from_slice(block);
    }

    debug_assert_eq!(
        bytes.len() as u64,
        total_size,
        "encoder produced wrong total length"
    );

    Ok(EncodedPayload { bytes, block_count })
}

#[allow(dead_code)]
const fn _assert_uncompressed_sentinel() -> u32 {
    BLOCK_TYPE_UNCOMPRESSED
}

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

    #[test]
    fn align_up_no_change_on_aligned() {
        assert_eq!(align_up(0), 0);
        assert_eq!(align_up(128), 128);
        assert_eq!(align_up(256), 256);
    }

    #[test]
    fn align_up_rounds_to_next_boundary() {
        assert_eq!(align_up(1), 128);
        assert_eq!(align_up(127), 128);
        assert_eq!(align_up(129), 256);
    }

    #[test]
    fn encode_round_trips_via_reader_for_small_payload() {
        let payload = (0u8..=200).collect::<Vec<_>>();
        let encoded = encode_standard(&payload).unwrap();
        let decoded = decode_via_reader(&encoded.bytes);
        assert_eq!(decoded, payload);
    }

    #[test]
    fn encode_round_trips_for_empty_payload() {
        let encoded = encode_standard(&[]).unwrap();
        let decoded = decode_via_reader(&encoded.bytes);
        assert!(decoded.is_empty());
    }

    #[test]
    fn encode_block_count_matches_chunk_split() {
        let payload = vec![0u8; (APPEND_BLOCK_SIZE as usize) * 3 + 100];
        let encoded = encode_standard(&payload).unwrap();
        assert_eq!(encoded.block_count, 4);
    }

    #[test]
    fn encode_aligns_blocks_to_128_bytes() {
        let payload = vec![42u8; (APPEND_BLOCK_SIZE as usize) + 100];
        let encoded = encode_standard(&payload).unwrap();
        let block0_offset = u32::from_le_bytes(encoded.bytes[24..28].try_into().unwrap());
        let block0_size = u16::from_le_bytes(encoded.bytes[28..30].try_into().unwrap());
        let block1_offset = u32::from_le_bytes(encoded.bytes[32..36].try_into().unwrap());
        let block1_size = u16::from_le_bytes(encoded.bytes[36..38].try_into().unwrap());
        assert_eq!(block0_offset, 0);
        assert_eq!(block0_offset % BLOCK_ALIGNMENT, 0);
        assert_eq!(u32::from(block0_size) % BLOCK_ALIGNMENT, 0);
        assert_eq!(block1_offset, u32::from(block0_size));
        assert_eq!(block1_offset % BLOCK_ALIGNMENT, 0);
        assert_eq!(u32::from(block1_size) % BLOCK_ALIGNMENT, 0);
    }

    #[test]
    fn encoded_file_info_advertises_correct_kind_and_block_count() {
        let payload = vec![0u8; (APPEND_BLOCK_SIZE as usize) * 2];
        let encoded = encode_standard(&payload).unwrap();
        let kind = u32::from_le_bytes(encoded.bytes[4..8].try_into().unwrap());
        let raw_file_size = u32::from_le_bytes(encoded.bytes[8..12].try_into().unwrap());
        let number_of_blocks = u32::from_le_bytes(encoded.bytes[20..24].try_into().unwrap());
        assert_eq!(kind, FileKind::Standard.as_raw());
        assert_eq!(u64::from(raw_file_size), payload.len() as u64);
        assert_eq!(number_of_blocks, 2);
    }

    fn decode_via_reader(payload_bytes: &[u8]) -> Vec<u8> {
        let mut dat = Vec::new();
        dat.extend_from_slice(b"SqPack\0\0");
        dat.push(0);
        dat.extend_from_slice(&[0, 0, 0]);
        dat.extend_from_slice(&1024u32.to_le_bytes());
        dat.extend_from_slice(&1u32.to_le_bytes());
        dat.extend_from_slice(&1u32.to_le_bytes());
        dat.resize(1024, 0);
        let payload_offset = dat.len() as u64;
        dat.extend_from_slice(payload_bytes);

        let fs = crate::vfs::InMemoryFs::new();
        let path = std::path::Path::new("/install/sqpack/ffxiv/0a0000.win32.dat0");
        fs.insert(path, dat);
        let mut df = crate::dat::DatFile::open_in(path, &fs).unwrap();
        df.read_file(payload_offset).unwrap()
    }
}
