//! Bounded native TGA and DDS decoding plus reference-compatible TGA output.

use crate::Error;
use libremetaverse_imaging::{DEFAULT_MAX_ENCODED_BYTES, ManagedImage, ManagedImageImageChannels};
use libremetaverse_types::compat::ReadWrite;
use std::io::Read as _;

type TgaPalette = (usize, Vec<[u8; 4]>);

/// Truevision TGA entry points backed by native TGA/DDS parsing.
pub struct Targa;

impl Targa {
    /// Decodes a TGA or DDS file into planar managed-image storage.
    ///
    /// # Errors
    ///
    /// Returns a typed error for inaccessible, oversized, malformed, or
    /// unsupported input and for invalid decoded dimensions.
    pub fn decode_to_managed_image_with_string(file_name: String) -> Result<ManagedImage, Error> {
        let mut file = std::fs::File::open(file_name).map_err(|_| Error::InvalidOperation)?;
        let bytes = read_bounded(&mut file)?;
        decode(&bytes)
    }

    /// Decodes a TGA or DDS stream into planar managed-image storage.
    ///
    /// # Errors
    ///
    /// Returns a typed error for failed reads, oversized input, malformed or
    /// unsupported data, and invalid decoded dimensions.
    pub fn decode_to_managed_image_with_stream(
        mut stream: Box<dyn ReadWrite + Send>,
    ) -> Result<ManagedImage, Error> {
        let bytes = read_bounded(&mut *stream)?;
        decode(&bytes)
    }

    /// Encodes the exact compact TGA layout emitted by the C# implementation.
    ///
    /// # Errors
    ///
    /// Returns a typed error for inconsistent planes, unsupported channel
    /// combinations, dimensions outside the TGA range, or allocation limits.
    #[allow(clippy::needless_pass_by_value)] // Fixed mapped C# value signature.
    pub fn encode(image: ManagedImage) -> Result<Vec<u8>, Error> {
        image.validate()?;
        let width = u16::try_from(image.width).map_err(|_| Error::Argument)?;
        let height = u16::try_from(image.height).map_err(|_| Error::Argument)?;
        let color = image.channels.contains(ManagedImageImageChannels::COLOR);
        let gray = image.channels.contains(ManagedImageImageChannels::GRAY);
        let alpha = image.channels.contains(ManagedImageImageChannels::ALPHA);
        let supported = color && !gray || !color && (gray || alpha);
        if !supported {
            return Err(Error::InvalidOperation);
        }
        let components = (if color { 3 } else { usize::from(gray) }) + usize::from(alpha);
        let pixels = usize::from(width)
            .checked_mul(usize::from(height))
            .ok_or(Error::Argument)?;
        let length = pixels
            .checked_mul(components)
            .and_then(|value| value.checked_add(32))
            .ok_or(Error::Argument)?;
        if length > DEFAULT_MAX_ENCODED_BYTES {
            return Err(Error::Argument);
        }
        let written_components = if alpha && !color { 4 } else { components };
        let written_end = pixels
            .checked_mul(written_components)
            .and_then(|value| value.checked_add(18))
            .ok_or(Error::Argument)?;
        if written_end > length {
            return Err(Error::InvalidOperation);
        }
        let mut output = vec![0; length];
        output[2] = 2;
        output[12..14].copy_from_slice(&width.to_le_bytes());
        output[14..16].copy_from_slice(&height.to_le_bytes());
        output[16] = u8::try_from(components * 8).map_err(|_| Error::Argument)?;
        output[17] = if alpha { 0x20 } else { 0 };
        let mut target = 18;
        for index in 0..pixels {
            if color {
                output[target] = image.blue[index];
                output[target + 1] = image.green[index];
                output[target + 2] = image.red[index];
                target += 3;
                if alpha {
                    output[target] = image.alpha[index];
                    target += 1;
                }
            } else if alpha {
                output[target] = image.alpha[index];
                output[target + 1] = image.alpha[index];
                output[target + 2] = image.alpha[index];
                output[target + 3] = u8::MAX;
                target += 4;
            } else {
                output[target] = image.red[index];
                target += 1;
            }
        }
        Ok(output)
    }
}

fn read_bounded<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Vec<u8>, Error> {
    let mut bytes = Vec::new();
    reader
        .take((DEFAULT_MAX_ENCODED_BYTES + 1) as u64)
        .read_to_end(&mut bytes)
        .map_err(|_| Error::InvalidOperation)?;
    if bytes.len() > DEFAULT_MAX_ENCODED_BYTES {
        Err(Error::Argument)
    } else {
        Ok(bytes)
    }
}

fn decode(bytes: &[u8]) -> Result<ManagedImage, Error> {
    if bytes.len() > DEFAULT_MAX_ENCODED_BYTES {
        return Err(Error::Argument);
    }
    if bytes.starts_with(b"DDS ") {
        decode_dds(bytes)
    } else {
        decode_tga(bytes)
    }
}

fn parse(position: usize, context: &'static str) -> Error {
    Error::Parse { position, context }
}

fn read_u16(bytes: &[u8], position: usize, context: &'static str) -> Result<u16, Error> {
    let value = bytes
        .get(position..position + 2)
        .ok_or_else(|| parse(position, context))?;
    Ok(u16::from_le_bytes([value[0], value[1]]))
}

fn read_u32(bytes: &[u8], position: usize, context: &'static str) -> Result<u32, Error> {
    let value = bytes
        .get(position..position + 4)
        .ok_or_else(|| parse(position, context))?;
    Ok(u32::from_le_bytes([value[0], value[1], value[2], value[3]]))
}

fn decode_tga(bytes: &[u8]) -> Result<ManagedImage, Error> {
    let header = bytes
        .get(..18)
        .ok_or_else(|| parse(0, "truncated TGA header"))?;
    let id_length = usize::from(header[0]);
    let image_type = header[2];
    let color_mapped = matches!(image_type, 1 | 9);
    let has_color_map = header[1] == 1;
    if header[1] > 1 || color_mapped && !has_color_map {
        return Err(parse(1, "invalid TGA color-map declaration"));
    }
    let rle = matches!(image_type, 9..=11);
    let grayscale = matches!(image_type, 3 | 11);
    if !matches!(image_type, 1 | 2 | 3 | 9 | 10 | 11) {
        return Err(parse(2, "unsupported TGA image type"));
    }
    let width = read_u16(bytes, 12, "truncated TGA width")?;
    let height = read_u16(bytes, 14, "truncated TGA height")?;
    let depth = header[16];
    if (grayscale && !matches!(depth, 8 | 16))
        || (color_mapped && !matches!(depth, 8 | 16))
        || (!grayscale && !color_mapped && !matches!(depth, 15 | 16 | 24 | 32))
    {
        return Err(parse(16, "unsupported TGA pixel depth"));
    }
    let palette_depth = header[7];
    if has_color_map && !matches!(palette_depth, 15 | 16 | 24 | 32) {
        return Err(parse(7, "unsupported TGA color-map depth"));
    }
    let channels = tga_channels(grayscale, color_mapped, depth, palette_depth, header[17]);
    let mut image = ManagedImage::new(i32::from(width), i32::from(height), channels)?;
    let pixels = usize::from(width) * usize::from(height);
    let pixel_bytes = usize::from(depth.div_ceil(8));
    let mut position = 18usize.checked_add(id_length).ok_or(Error::Argument)?;
    if position > bytes.len() {
        return Err(parse(18, "truncated TGA image ID"));
    }
    let palette = read_tga_palette(bytes, header, has_color_map, color_mapped, &mut position)?;
    let mut decoded = 0;
    while decoded < pixels {
        let (count, repeated) = if rle {
            let packet = *bytes
                .get(position)
                .ok_or_else(|| parse(position, "truncated TGA RLE packet"))?;
            position += 1;
            (usize::from(packet & 0x7f) + 1, packet & 0x80 != 0)
        } else {
            (1, false)
        };
        if count > pixels - decoded {
            return Err(parse(position, "TGA RLE packet exceeds pixel count"));
        }
        if repeated {
            let pixel = bytes
                .get(position..position + pixel_bytes)
                .ok_or_else(|| parse(position, "truncated TGA pixel"))?;
            position += pixel_bytes;
            for _ in 0..count {
                write_tga_pixel(
                    &mut image,
                    decoded,
                    pixel,
                    depth,
                    header[17],
                    grayscale,
                    palette.as_ref(),
                )?;
                decoded += 1;
            }
        } else {
            for _ in 0..count {
                let pixel = bytes
                    .get(position..position + pixel_bytes)
                    .ok_or_else(|| parse(position, "truncated TGA pixel"))?;
                position += pixel_bytes;
                write_tga_pixel(
                    &mut image,
                    decoded,
                    pixel,
                    depth,
                    header[17],
                    grayscale,
                    palette.as_ref(),
                )?;
                decoded += 1;
            }
        }
    }
    Ok(image)
}

fn tga_channels(
    grayscale: bool,
    color_mapped: bool,
    depth: u8,
    palette_depth: u8,
    descriptor: u8,
) -> ManagedImageImageChannels {
    if grayscale {
        if depth == 16 {
            ManagedImageImageChannels::GRAY | ManagedImageImageChannels::ALPHA
        } else {
            ManagedImageImageChannels::GRAY
        }
    } else if depth == 32
        || color_mapped && palette_depth == 32
        || descriptor & 0x0f != 0 && (depth == 16 || color_mapped && palette_depth == 16)
    {
        ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA
    } else {
        ManagedImageImageChannels::COLOR
    }
}

fn read_tga_palette(
    bytes: &[u8],
    header: &[u8],
    has_color_map: bool,
    apply_palette: bool,
    position: &mut usize,
) -> Result<Option<TgaPalette>, Error> {
    if !has_color_map {
        return Ok(None);
    }
    let first = usize::from(read_u16(bytes, 3, "truncated TGA color-map origin")?);
    let length = usize::from(read_u16(bytes, 5, "truncated TGA color-map length")?);
    if apply_palette && length == 0 {
        return Err(parse(5, "empty TGA color map"));
    }
    let palette_depth = header[7];
    let entry_bytes = usize::from(palette_depth.div_ceil(8));
    let byte_length = length.checked_mul(entry_bytes).ok_or(Error::Argument)?;
    let palette_end = position.checked_add(byte_length).ok_or(Error::Argument)?;
    let data = bytes
        .get(*position..palette_end)
        .ok_or_else(|| parse(*position, "truncated TGA color map"))?;
    *position = palette_end;
    if !apply_palette {
        return Ok(None);
    }
    let mut entries = Vec::with_capacity(length);
    for entry in data.chunks_exact(entry_bytes) {
        entries.push(decode_tga_color(entry, palette_depth)?);
    }
    Ok(Some((first, entries)))
}

fn write_tga_pixel(
    image: &mut ManagedImage,
    file_index: usize,
    pixel: &[u8],
    depth: u8,
    descriptor: u8,
    grayscale: bool,
    palette: Option<&TgaPalette>,
) -> Result<(), Error> {
    let width = usize::try_from(image.width).map_err(|_| Error::Argument)?;
    let height = usize::try_from(image.height).map_err(|_| Error::Argument)?;
    let file_x = file_index % width;
    let file_y = file_index / width;
    let x = if descriptor & 0x10 == 0 {
        file_x
    } else {
        width - 1 - file_x
    };
    let y = if descriptor & 0x20 != 0 {
        file_y
    } else {
        height - 1 - file_y
    };
    let target = y * width + x;
    if let Some((first, entries)) = palette {
        let index = if depth == 8 {
            usize::from(pixel[0])
        } else {
            usize::from(u16::from_le_bytes([pixel[0], pixel[1]]))
        };
        let color = entries
            .get(
                index
                    .checked_sub(*first)
                    .ok_or_else(|| parse(18, "TGA color-map index below origin"))?,
            )
            .ok_or_else(|| parse(18, "TGA color-map index out of range"))?;
        image.red[target] = color[0];
        image.green[target] = color[1];
        image.blue[target] = color[2];
        if !image.alpha.is_empty() {
            image.alpha[target] = color[3];
        }
        return Ok(());
    }
    if grayscale {
        image.red[target] = pixel[0];
        if depth == 16 {
            image.alpha[target] = pixel[1];
        }
        return Ok(());
    }
    match depth {
        15 | 16 => {
            let value = u16::from_le_bytes([pixel[0], pixel[1]]);
            image.red[target] = expand_5(u8::try_from((value >> 10) & 31).unwrap());
            image.green[target] = expand_5(u8::try_from((value >> 5) & 31).unwrap());
            image.blue[target] = expand_5(u8::try_from(value & 31).unwrap());
            if !image.alpha.is_empty() {
                image.alpha[target] = if value & 0x8000 == 0 { 0 } else { u8::MAX };
            }
        }
        24 | 32 => {
            image.blue[target] = pixel[0];
            image.green[target] = pixel[1];
            image.red[target] = pixel[2];
            if depth == 32 {
                image.alpha[target] = pixel[3];
            }
        }
        _ => return Err(Error::Argument),
    }
    Ok(())
}

fn decode_tga_color(pixel: &[u8], depth: u8) -> Result<[u8; 4], Error> {
    match depth {
        15 | 16 => {
            let value = u16::from_le_bytes([pixel[0], pixel[1]]);
            Ok([
                expand_5(u8::try_from((value >> 10) & 31).unwrap()),
                expand_5(u8::try_from((value >> 5) & 31).unwrap()),
                expand_5(u8::try_from(value & 31).unwrap()),
                if depth == 16 && value & 0x8000 == 0 {
                    0
                } else {
                    u8::MAX
                },
            ])
        }
        24 => Ok([pixel[2], pixel[1], pixel[0], 255]),
        32 => Ok([pixel[2], pixel[1], pixel[0], pixel[3]]),
        _ => Err(Error::Argument),
    }
}

fn expand_5(value: u8) -> u8 {
    u8::try_from((u16::from(value) * 255 + 15) / 31).unwrap()
}

fn expand_6(value: u8) -> u8 {
    u8::try_from((u16::from(value) * 255 + 31) / 63).unwrap()
}

fn decode_dds(bytes: &[u8]) -> Result<ManagedImage, Error> {
    if bytes.len() < 128 || read_u32(bytes, 4, "truncated DDS header")? != 124 {
        return Err(parse(4, "invalid DDS header"));
    }
    if read_u32(bytes, 76, "truncated DDS pixel format")? != 32 {
        return Err(parse(76, "invalid DDS pixel format"));
    }
    let height = read_u32(bytes, 12, "truncated DDS height")?;
    let width = read_u32(bytes, 16, "truncated DDS width")?;
    let width_i32 = i32::try_from(width).map_err(|_| Error::Argument)?;
    let height_i32 = i32::try_from(height).map_err(|_| Error::Argument)?;
    let fourcc = bytes
        .get(84..88)
        .ok_or_else(|| parse(84, "truncated DDS FourCC"))?;
    if fourcc == b"DX10" {
        let format = read_u32(bytes, 128, "truncated DDS DX10 header")?;
        return decode_dds_dx10(bytes, width_i32, height_i32, format);
    }
    let block = match fourcc {
        [0, 0, 0, 0] => {
            let bits = read_u32(bytes, 88, "truncated DDS bit count")?;
            let masks = [
                read_u32(bytes, 92, "truncated DDS red mask")?,
                read_u32(bytes, 96, "truncated DDS green mask")?,
                read_u32(bytes, 100, "truncated DDS blue mask")?,
                read_u32(bytes, 104, "truncated DDS alpha mask")?,
            ];
            return decode_dds_uncompressed(bytes, width_i32, height_i32, 128, bits, masks);
        }
        b"DXT1" => DdsBlock::Bc1,
        b"DXT3" => DdsBlock::Bc2,
        b"DXT5" => DdsBlock::Bc3,
        b"ATI1" | b"BC4U" => DdsBlock::Bc4 { signed: false },
        b"BC4S" => DdsBlock::Bc4 { signed: true },
        b"ATI2" | b"BC5U" => DdsBlock::Bc5 { signed: false },
        b"BC5S" => DdsBlock::Bc5 { signed: true },
        _ => return Err(parse(84, "unsupported DDS compression")),
    };
    decode_dds_blocks(bytes, width_i32, height_i32, 128, block)
}

fn decode_dds_dx10(
    bytes: &[u8],
    width: i32,
    height: i32,
    format: u32,
) -> Result<ManagedImage, Error> {
    match format {
        27..=32 => decode_dds_uncompressed(
            bytes,
            width,
            height,
            148,
            32,
            [0x0000_00ff, 0x0000_ff00, 0x00ff_0000, 0xff00_0000],
        ),
        87 | 90 | 91 | 93 => decode_dds_uncompressed(
            bytes,
            width,
            height,
            148,
            32,
            [0x00ff_0000, 0x0000_ff00, 0x0000_00ff, 0xff00_0000],
        ),
        70..=72 => decode_dds_blocks(bytes, width, height, 148, DdsBlock::Bc1),
        73..=75 => decode_dds_blocks(bytes, width, height, 148, DdsBlock::Bc2),
        76..=78 => decode_dds_blocks(bytes, width, height, 148, DdsBlock::Bc3),
        79 | 80 => decode_dds_blocks(bytes, width, height, 148, DdsBlock::Bc4 { signed: false }),
        81 => decode_dds_blocks(bytes, width, height, 148, DdsBlock::Bc4 { signed: true }),
        82 | 83 => decode_dds_blocks(bytes, width, height, 148, DdsBlock::Bc5 { signed: false }),
        84 => decode_dds_blocks(bytes, width, height, 148, DdsBlock::Bc5 { signed: true }),
        94 | 95 => decode_dds_blocks(bytes, width, height, 148, DdsBlock::Bc6 { signed: false }),
        96 => decode_dds_blocks(bytes, width, height, 148, DdsBlock::Bc6 { signed: true }),
        97..=99 => decode_dds_blocks(bytes, width, height, 148, DdsBlock::Bc7),
        _ => Err(parse(128, "unsupported DDS DXGI format")),
    }
}

fn decode_dds_uncompressed(
    bytes: &[u8],
    width: i32,
    height: i32,
    data_offset: usize,
    bits: u32,
    mut masks: [u32; 4],
) -> Result<ManagedImage, Error> {
    let bytes_per_pixel = usize::try_from(bits.div_ceil(8)).map_err(|_| Error::Argument)?;
    if !matches!(bits, 8 | 16 | 24 | 32) {
        return Err(parse(88, "unsupported DDS pixel depth"));
    }
    if bits == 16 && masks[1] != 0x07e0 {
        return Err(parse(96, "unsupported DDS 16-bit layout"));
    }
    if bits >= 24 && masks[..3].contains(&0) {
        masks[..3].copy_from_slice(&[0x00ff_0000, 0x0000_ff00, 0x0000_00ff]);
    }
    if bits == 32 && masks[3] == 0 {
        masks[3] = 0xff00_0000;
    }
    let channels = if bits == 8 {
        ManagedImageImageChannels::GRAY
    } else if bits == 32 {
        ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA
    } else {
        ManagedImageImageChannels::COLOR
    };
    let mut image = ManagedImage::new(width, height, channels)?;
    let width_usize = usize::try_from(width).map_err(|_| Error::Argument)?;
    let height_usize = usize::try_from(height).map_err(|_| Error::Argument)?;
    let packed_row = width_usize
        .checked_mul(bytes_per_pixel)
        .ok_or(Error::Argument)?;
    let required = data_offset
        .checked_add(
            packed_row
                .checked_mul(height_usize)
                .ok_or(Error::Argument)?,
        )
        .ok_or(Error::Argument)?;
    if required > bytes.len() {
        return Err(parse(bytes.len(), "truncated DDS pixel data"));
    }
    for y in 0..height_usize {
        for x in 0..width_usize {
            let source = data_offset + y * packed_row + x * bytes_per_pixel;
            let raw = bytes[source..source + bytes_per_pixel]
                .iter()
                .enumerate()
                .fold(0u32, |value, (shift, byte)| {
                    value | (u32::from(*byte) << (shift * 8))
                });
            let target = y * width_usize + x;
            if bits == 8 {
                image.red[target] = u8::try_from(raw).unwrap();
            } else {
                image.red[target] = extract_mask(raw, masks[0], bits)?;
                image.green[target] = extract_mask(raw, masks[1], bits)?;
                image.blue[target] = extract_mask(raw, masks[2], bits)?;
                if bits == 32 {
                    image.alpha[target] = extract_mask(raw, masks[3], bits)?;
                }
            }
        }
    }
    Ok(image)
}

fn extract_mask(value: u32, mask: u32, bits: u32) -> Result<u8, Error> {
    if mask == 0 {
        if bits == 8 {
            return Ok(u8::try_from(value & 0xff).unwrap());
        }
        return Err(parse(92, "missing DDS channel mask"));
    }
    let shift = mask.trailing_zeros();
    let maximum = mask >> shift;
    let component = (value & mask) >> shift;
    let numerator = u64::from(component) * 255 + u64::from(maximum) / 2;
    Ok(u8::try_from(numerator / u64::from(maximum)).unwrap())
}

#[derive(Clone, Copy)]
enum DdsBlock {
    Bc1,
    Bc2,
    Bc3,
    Bc4 { signed: bool },
    Bc5 { signed: bool },
    Bc6 { signed: bool },
    Bc7,
}

impl DdsBlock {
    const fn encoded_bytes(self) -> usize {
        match self {
            Self::Bc1 | Self::Bc4 { .. } => 8,
            Self::Bc2 | Self::Bc3 | Self::Bc5 { .. } | Self::Bc6 { .. } | Self::Bc7 => 16,
        }
    }

    const fn channels(self) -> ManagedImageImageChannels {
        match self {
            Self::Bc4 { .. } => ManagedImageImageChannels::GRAY,
            Self::Bc5 { .. } => ManagedImageImageChannels::COLOR,
            Self::Bc1 | Self::Bc2 | Self::Bc3 | Self::Bc6 { .. } | Self::Bc7 => {
                ManagedImageImageChannels(
                    ManagedImageImageChannels::COLOR.0 | ManagedImageImageChannels::ALPHA.0,
                )
            }
        }
    }
}

fn decode_dds_blocks(
    bytes: &[u8],
    width: i32,
    height: i32,
    data_offset: usize,
    format: DdsBlock,
) -> Result<ManagedImage, Error> {
    let block_bytes = format.encoded_bytes();
    let mut image = ManagedImage::new(width, height, format.channels())?;
    let width = usize::try_from(width).map_err(|_| Error::Argument)?;
    let height = usize::try_from(height).map_err(|_| Error::Argument)?;
    let blocks_x = width.div_ceil(4);
    let blocks_y = height.div_ceil(4);
    let length = blocks_x
        .checked_mul(blocks_y)
        .and_then(|count| count.checked_mul(block_bytes))
        .ok_or(Error::Argument)?;
    let data_end = data_offset.checked_add(length).ok_or(Error::Argument)?;
    let data = bytes
        .get(data_offset..data_end)
        .ok_or_else(|| parse(data_offset, "truncated DDS block data"))?;
    for block_y in 0..blocks_y {
        for block_x in 0..blocks_x {
            let start = (block_y * blocks_x + block_x) * block_bytes;
            decode_dds_block(
                &data[start..start + block_bytes],
                format,
                block_x,
                block_y,
                width,
                height,
                &mut image,
            )?;
        }
    }
    Ok(image)
}

#[allow(clippy::too_many_arguments)]
fn decode_dds_block(
    block: &[u8],
    format: DdsBlock,
    block_x: usize,
    block_y: usize,
    width: usize,
    height: usize,
    image: &mut ManagedImage,
) -> Result<(), Error> {
    match format {
        DdsBlock::Bc1 => decode_dxt_block(block, 1, block_x, block_y, width, height, image),
        DdsBlock::Bc2 => decode_dxt_block(block, 3, block_x, block_y, width, height, image),
        DdsBlock::Bc3 => decode_dxt_block(block, 5, block_x, block_y, width, height, image),
        DdsBlock::Bc4 { signed } => {
            decode_bc4_block(block, signed, block_x, block_y, width, height, image);
            Ok(())
        }
        DdsBlock::Bc5 { signed } => {
            decode_bc5_block(block, signed, block_x, block_y, width, height, image);
            Ok(())
        }
        DdsBlock::Bc6 { signed } => {
            decode_bc6_block(block, signed, block_x, block_y, width, height, image)
        }
        DdsBlock::Bc7 => decode_bc7_block(block, block_x, block_y, width, height, image),
    }
}

#[allow(clippy::too_many_arguments)]
fn decode_dxt_block(
    block: &[u8],
    alpha_kind: u8,
    block_x: usize,
    block_y: usize,
    width: usize,
    height: usize,
    image: &mut ManagedImage,
) -> Result<(), Error> {
    let color_offset = if alpha_kind == 1 { 0 } else { 8 };
    let color0 = u16::from_le_bytes([block[color_offset], block[color_offset + 1]]);
    let color1 = u16::from_le_bytes([block[color_offset + 2], block[color_offset + 3]]);
    let mut color_table = [rgb565(color0), rgb565(color1), [0; 3], [0; 3]];
    if color0 > color1 || alpha_kind != 1 {
        color_table[2] = blend_rgb565(color0, color1, 2, 1, 3);
        color_table[3] = blend_rgb565(color0, color1, 1, 2, 3);
    } else {
        color_table[2] = blend_rgb565(color0, color1, 1, 1, 2);
    }
    let selectors = u32::from_le_bytes([
        block[color_offset + 4],
        block[color_offset + 5],
        block[color_offset + 6],
        block[color_offset + 7],
    ]);
    let alphas = dxt_alphas(block, alpha_kind)?;
    for local_y in 0..4 {
        for local_x in 0..4 {
            let x = block_x * 4 + local_x;
            let y = block_y * 4 + local_y;
            if x >= width || y >= height {
                continue;
            }
            let local = local_y * 4 + local_x;
            let selector = usize::try_from((selectors >> (local * 2)) & 3).unwrap();
            let target = y * width + x;
            image.red[target] = color_table[selector][0];
            image.green[target] = color_table[selector][1];
            image.blue[target] = color_table[selector][2];
            image.alpha[target] = if alpha_kind == 1 && color0 <= color1 && selector == 3 {
                0
            } else {
                alphas[local]
            };
        }
    }
    Ok(())
}

#[allow(clippy::too_many_arguments)]
fn decode_bc4_block(
    block: &[u8],
    signed: bool,
    block_x: usize,
    block_y: usize,
    width: usize,
    height: usize,
    image: &mut ManagedImage,
) {
    let values = bc4_values(block, signed, true);
    let selectors = bc_selectors(block);
    for local_y in 0..4 {
        for local_x in 0..4 {
            let x = block_x * 4 + local_x;
            let y = block_y * 4 + local_y;
            if x < width && y < height {
                let local = local_y * 4 + local_x;
                let selector = usize::try_from((selectors >> (local * 3)) & 7).unwrap();
                image.red[y * width + x] = values[selector];
            }
        }
    }
}

#[allow(clippy::too_many_arguments)]
fn decode_bc5_block(
    block: &[u8],
    signed: bool,
    block_x: usize,
    block_y: usize,
    width: usize,
    height: usize,
    image: &mut ManagedImage,
) {
    let red = bc4_values(block, signed, false);
    let green = bc4_values(&block[8..], signed, false);
    let red_selectors = bc_selectors(block);
    let green_selectors = bc_selectors(&block[8..]);
    for local_y in 0..4 {
        for local_x in 0..4 {
            let x = block_x * 4 + local_x;
            let y = block_y * 4 + local_y;
            if x < width && y < height {
                let local = local_y * 4 + local_x;
                let target = y * width + x;
                let red_selector = usize::try_from((red_selectors >> (local * 3)) & 7).unwrap();
                let green_selector = usize::try_from((green_selectors >> (local * 3)) & 7).unwrap();
                image.red[target] = red[red_selector];
                image.green[target] = green[green_selector];
            }
        }
    }
}

fn bc_selectors(block: &[u8]) -> u64 {
    block[2..8]
        .iter()
        .enumerate()
        .fold(0, |value, (index, byte)| {
            value | (u64::from(*byte) << (index * 8))
        })
}

#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn bc4_values(block: &[u8], signed: bool, rounded_unsigned: bool) -> [u8; 8] {
    if signed {
        let first = f32::from(i8::from_ne_bytes([block[0]]).max(-127));
        let second = f32::from(i8::from_ne_bytes([block[1]]).max(-127));
        let mut values = [0.0f32; 8];
        values[0] = first;
        values[1] = second;
        if first > second {
            for index in 1u8..=6 {
                values[usize::from(index + 1)] =
                    (f32::from(7 - index) * first + f32::from(index) * second) / 7.0;
            }
        } else {
            for index in 1u8..=4 {
                values[usize::from(index + 1)] =
                    (f32::from(5 - index) * first + f32::from(index) * second) / 5.0;
            }
            values[6] = -127.0;
            values[7] = 127.0;
        }
        return values.map(|value| {
            let normalized = ((value + 127.0) * (255.0 / 254.0)) + 0.5;
            normalized as u8
        });
    }

    let first = usize::from(block[0]);
    let second = usize::from(block[1]);
    let mut values = [0; 8];
    values[0] = block[0];
    values[1] = block[1];
    if first > second {
        for index in 1..=6 {
            let numerator = (7 - index) * first + index * second;
            values[index + 1] = u8::try_from(if rounded_unsigned {
                (numerator + 3) / 7
            } else {
                numerator / 7
            })
            .unwrap();
        }
    } else {
        for index in 1..=4 {
            let numerator = (5 - index) * first + index * second;
            values[index + 1] = u8::try_from(if rounded_unsigned {
                (numerator + 2) / 5
            } else {
                numerator / 5
            })
            .unwrap();
        }
        values[6] = 0;
        values[7] = u8::MAX;
    }
    values
}

#[allow(clippy::too_many_arguments, clippy::unnecessary_wraps)]
fn decode_bc6_block(
    block: &[u8],
    signed: bool,
    block_x: usize,
    block_y: usize,
    width: usize,
    height: usize,
    image: &mut ManagedImage,
) -> Result<(), Error> {
    #[cfg(not(feature = "dds-bc67"))]
    {
        let _ = (block, signed, block_x, block_y, width, height, image);
        Err(parse(128, "DDS BC6H support is disabled"))
    }
    #[cfg(feature = "dds-bc67")]
    {
        let mut decoded = [0.0f32; 4 * 4 * 3];
        bcdec_rs::bc6h_float(block, &mut decoded, 4 * 3, signed);
        for local_y in 0..4 {
            for local_x in 0..4 {
                let x = block_x * 4 + local_x;
                let y = block_y * 4 + local_y;
                if x < width && y < height {
                    let source = (local_y * 4 + local_x) * 3;
                    let target = y * width + x;
                    image.red[target] = float_channel(decoded[source]);
                    image.green[target] = float_channel(decoded[source + 1]);
                    image.blue[target] = float_channel(decoded[source + 2]);
                    image.alpha[target] = u8::MAX;
                }
            }
        }
        Ok(())
    }
}

#[cfg(feature = "dds-bc67")]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn float_channel(value: f32) -> u8 {
    (value.clamp(0.0, 1.0) * 255.0 + 0.5) as u8
}

#[allow(clippy::too_many_arguments, clippy::unnecessary_wraps)]
fn decode_bc7_block(
    block: &[u8],
    block_x: usize,
    block_y: usize,
    width: usize,
    height: usize,
    image: &mut ManagedImage,
) -> Result<(), Error> {
    #[cfg(not(feature = "dds-bc67"))]
    {
        let _ = (block, block_x, block_y, width, height, image);
        Err(parse(128, "DDS BC7 support is disabled"))
    }
    #[cfg(feature = "dds-bc67")]
    {
        let mut decoded = [0u8; 4 * 4 * 4];
        bcdec_rs::bc7(block, &mut decoded, 4 * 4);
        for local_y in 0..4 {
            for local_x in 0..4 {
                let x = block_x * 4 + local_x;
                let y = block_y * 4 + local_y;
                if x < width && y < height {
                    let source = (local_y * 4 + local_x) * 4;
                    let target = y * width + x;
                    image.red[target] = decoded[source];
                    image.green[target] = decoded[source + 1];
                    image.blue[target] = decoded[source + 2];
                    image.alpha[target] = decoded[source + 3];
                }
            }
        }
        Ok(())
    }
}

fn blend_rgb565(
    first: u16,
    second: u16,
    first_weight: u16,
    second_weight: u16,
    divisor: u16,
) -> [u8; 3] {
    let components = [
        ((first >> 11) & 31, (second >> 11) & 31, 31),
        ((first >> 5) & 63, (second >> 5) & 63, 63),
        (first & 31, second & 31, 31),
    ];
    components.map(|(first, second, maximum)| {
        let numerator = u32::from(first_weight * first + second_weight * second) * 255;
        let denominator = u32::from(divisor * maximum);
        u8::try_from((numerator + denominator / 2) / denominator).unwrap()
    })
}

fn rgb565(value: u16) -> [u8; 3] {
    [
        expand_5(u8::try_from((value >> 11) & 31).unwrap()),
        expand_6(u8::try_from((value >> 5) & 63).unwrap()),
        expand_5(u8::try_from(value & 31).unwrap()),
    ]
}

fn dxt_alphas(block: &[u8], kind: u8) -> Result<[u8; 16], Error> {
    let mut output = [u8::MAX; 16];
    if kind == 3 {
        let bits = u64::from_le_bytes(block[..8].try_into().map_err(|_| Error::Argument)?);
        for (index, alpha) in output.iter_mut().enumerate() {
            let nibble = u8::try_from((bits >> (index * 4)) & 15).unwrap();
            *alpha = nibble * 17;
        }
    } else if kind == 5 {
        let alpha0 = block[0];
        let alpha1 = block[1];
        let mut table = [0; 8];
        table[0] = alpha0;
        table[1] = alpha1;
        if alpha0 > alpha1 {
            for index in 1..=6 {
                table[index + 1] = u8::try_from(
                    ((7 - index) * usize::from(alpha0) + index * usize::from(alpha1)) / 7,
                )
                .unwrap();
            }
        } else {
            for index in 1..=4 {
                table[index + 1] = u8::try_from(
                    ((5 - index) * usize::from(alpha0) + index * usize::from(alpha1)) / 5,
                )
                .unwrap();
            }
            table[6] = 0;
            table[7] = u8::MAX;
        }
        let mut selectors = 0u64;
        for index in 0..6 {
            selectors |= u64::from(block[index + 2]) << (index * 8);
        }
        for (index, alpha) in output.iter_mut().enumerate() {
            *alpha = table[usize::try_from((selectors >> (index * 3)) & 7).unwrap()];
        }
    }
    Ok(output)
}

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

    fn tga_header(image_type: u8, width: u16, height: u16, depth: u8, descriptor: u8) -> Vec<u8> {
        let mut header = vec![0; 18];
        header[2] = image_type;
        header[12..14].copy_from_slice(&width.to_le_bytes());
        header[14..16].copy_from_slice(&height.to_le_bytes());
        header[16] = depth;
        header[17] = descriptor;
        header
    }

    fn dds_header(width: u32, height: u32, fourcc: [u8; 4], bits: u32) -> Vec<u8> {
        let mut header = vec![0; 128];
        header[..4].copy_from_slice(b"DDS ");
        header[4..8].copy_from_slice(&124u32.to_le_bytes());
        header[12..16].copy_from_slice(&height.to_le_bytes());
        header[16..20].copy_from_slice(&width.to_le_bytes());
        header[20..24].copy_from_slice(&(width * bits.div_ceil(8)).to_le_bytes());
        header[76..80].copy_from_slice(&32u32.to_le_bytes());
        header[80..84].copy_from_slice(&0x40u32.to_le_bytes());
        header[84..88].copy_from_slice(&fourcc);
        header[88..92].copy_from_slice(&bits.to_le_bytes());
        header
    }

    #[test]
    fn reference_tga_encoder_bytes_are_exact_and_deterministic() {
        let mut image = ManagedImage::new(1, 1, ManagedImageImageChannels::COLOR).unwrap();
        image.red[0] = 1;
        image.green[0] = 2;
        image.blue[0] = 3;
        let first = Targa::encode(image).unwrap();
        assert_eq!(first.len(), 35);
        assert_eq!(&first[..18], &tga_header(2, 1, 1, 24, 0));
        assert_eq!(&first[18..21], &[3, 2, 1]);
        assert!(first[21..].iter().all(|byte| *byte == 0));

        let mut image = ManagedImage::new(
            1,
            1,
            ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA,
        )
        .unwrap();
        image.red[0] = 10;
        image.green[0] = 20;
        image.blue[0] = 30;
        image.alpha[0] = 40;
        let encoded = Targa::encode(image).unwrap();
        assert_eq!(&encoded[..18], &tga_header(2, 1, 1, 32, 0x20));
        assert_eq!(&encoded[18..22], &[30, 20, 10, 40]);

        let mut alpha = ManagedImage::new(1, 1, ManagedImageImageChannels::ALPHA).unwrap();
        alpha.alpha[0] = 42;
        let encoded = Targa::encode(alpha).unwrap();
        assert_eq!(encoded.len(), 33);
        assert_eq!(&encoded[..18], &tga_header(2, 1, 1, 8, 0x20));
        assert_eq!(&encoded[18..22], &[42, 42, 42, 255]);
    }

    #[test]
    fn tga_decoder_preserves_origin_depth_and_rle_packets() {
        let mut bottom_left = tga_header(2, 2, 2, 24, 0);
        bottom_left.extend_from_slice(&[
            30, 20, 10, 60, 50, 40, // bottom row
            90, 80, 70, 120, 110, 100, // top row
        ]);
        let image = decode_tga(&bottom_left).unwrap();
        assert_eq!(image.red, [70, 100, 10, 40]);
        assert_eq!(image.green, [80, 110, 20, 50]);
        assert_eq!(image.blue, [90, 120, 30, 60]);

        let mut rle = tga_header(10, 4, 1, 24, 0x20);
        rle.extend_from_slice(&[0x83, 3, 2, 1]);
        let image = decode_tga(&rle).unwrap();
        assert_eq!(image.red, [1; 4]);
        assert_eq!(image.green, [2; 4]);
        assert_eq!(image.blue, [3; 4]);

        let mut indexed = tga_header(1, 2, 1, 8, 0x20);
        indexed[1] = 1;
        indexed[5..7].copy_from_slice(&2u16.to_le_bytes());
        indexed[7] = 24;
        indexed.extend_from_slice(&[0, 0, 255, 0, 255, 0, 0, 1]);
        let image = decode_tga(&indexed).unwrap();
        assert_eq!(image.red, [255, 0]);
        assert_eq!(image.green, [0, 255]);
        assert_eq!(image.blue, [0, 0]);

        let mut top_right = tga_header(2, 2, 1, 24, 0x30);
        top_right.extend_from_slice(&[0, 0, 255, 0, 255, 0]);
        let image = decode_tga(&top_right).unwrap();
        assert_eq!(image.red, [0, 255]);
        assert_eq!(image.green, [255, 0]);

        let mut gray_rle = tga_header(11, 3, 1, 8, 0x20);
        gray_rle.extend_from_slice(&[0x82, 9]);
        assert_eq!(decode_tga(&gray_rle).unwrap().red, [9; 3]);

        let mut gray_alpha = tga_header(3, 1, 1, 16, 0x28);
        gray_alpha.extend_from_slice(&[9, 200]);
        let image = decode_tga(&gray_alpha).unwrap();
        assert_eq!(image.red, [9]);
        assert_eq!(image.alpha, [200]);

        let mut rgba = tga_header(2, 1, 1, 32, 0x20);
        rgba.extend_from_slice(&[3, 2, 1, 4]);
        let image = decode_tga(&rgba).unwrap();
        assert_eq!((image.red[0], image.green[0], image.blue[0]), (1, 2, 3));
        assert_eq!(image.alpha, [4]);

        let mut rgb555 = tga_header(2, 1, 1, 16, 0x20);
        rgb555.extend_from_slice(&0x7c00u16.to_le_bytes());
        assert_eq!(decode_tga(&rgb555).unwrap().red, [255]);

        let mut argb1555 = tga_header(2, 1, 1, 16, 0x21);
        argb1555.extend_from_slice(&0xfc00u16.to_le_bytes());
        let image = decode_tga(&argb1555).unwrap();
        assert_eq!(image.red, [255]);
        assert_eq!(image.alpha, [255]);

        let mut ancillary_palette = tga_header(2, 1, 1, 24, 0x20);
        ancillary_palette[1] = 1;
        ancillary_palette[5..7].copy_from_slice(&1u16.to_le_bytes());
        ancillary_palette[7] = 24;
        ancillary_palette.extend_from_slice(&[99, 98, 97, 3, 2, 1]);
        assert_eq!(decode_tga(&ancillary_palette).unwrap().red, [1]);
    }

    #[test]
    fn dds_uncompressed_rgb565_and_dxt1_decode_exact_pixels() {
        let mut rgb565 = dds_header(2, 1, [0; 4], 16);
        rgb565[92..96].copy_from_slice(&0xf800u32.to_le_bytes());
        rgb565[96..100].copy_from_slice(&0x07e0u32.to_le_bytes());
        rgb565[100..104].copy_from_slice(&0x001fu32.to_le_bytes());
        rgb565.extend_from_slice(&[0x00, 0xf8, 0xe0, 0x07]);
        let image = decode_dds(&rgb565).unwrap();
        assert_eq!(image.red, [255, 0]);
        assert_eq!(image.green, [0, 255]);
        assert_eq!(image.blue, [0, 0]);

        let mut dxt1 = dds_header(4, 4, *b"DXT1", 0);
        dxt1.extend_from_slice(&[0x00, 0xf8, 0xe0, 0x07, 0, 0, 0, 0]);
        let image = decode_dds(&dxt1).unwrap();
        assert_eq!(image.red, [255; 16]);
        assert_eq!(image.green, [0; 16]);
        assert_eq!(image.blue, [0; 16]);
        assert_eq!(image.alpha, [255; 16]);

        let mut bc4 = dds_header(4, 4, *b"ATI1", 0);
        bc4.extend_from_slice(&[200, 100, 0, 0, 0, 0, 0, 0]);
        assert_eq!(decode_dds(&bc4).unwrap().red, [200; 16]);

        let mut bc5 = dds_header(4, 4, *b"ATI2", 0);
        bc5.extend_from_slice(&[200, 100, 0, 0, 0, 0, 0, 0]);
        bc5.extend_from_slice(&[40, 20, 0, 0, 0, 0, 0, 0]);
        let image = decode_dds(&bc5).unwrap();
        assert_eq!(image.red, [200; 16]);
        assert_eq!(image.green, [40; 16]);
        assert_eq!(image.blue, [0; 16]);

        let mut dx10_dds = dds_header(1, 1, *b"DX10", 0);
        dx10_dds.extend_from_slice(&28u32.to_le_bytes());
        dx10_dds.extend_from_slice(&[0; 16]);
        dx10_dds.extend_from_slice(&[1, 2, 3, 4]);
        let image = decode_dds(&dx10_dds).unwrap();
        assert_eq!(image.red, [1]);
        assert_eq!(image.green, [2]);
        assert_eq!(image.blue, [3]);
        assert_eq!(image.alpha, [4]);

        #[cfg(feature = "dds-bc67")]
        for (format, block, expected) in [
            (
                95u32,
                [
                    0xaf, 0xf4, 0xd2, 0xbd, 0x07, 0x07, 0x1c, 0xf0, 0, 0, 0, 0, 0, 0, 0, 0,
                ],
                [128, 128, 255, 255],
            ),
            (
                98u32,
                [
                    0xa0, 0x40, 0xe0, 0xff, 0xff, 0xff, 0x03, 0x02, 0x02, 0, 0, 0, 0, 0, 0, 0,
                ],
                [129, 128, 255, 255],
            ),
        ] {
            // Blocks and exact pixels are derived from Pfim 0.11.4's published fixtures.
            let mut compressed = dds_header(4, 4, *b"DX10", 0);
            compressed.extend_from_slice(&format.to_le_bytes());
            compressed.extend_from_slice(&[0; 16]);
            compressed.extend_from_slice(&block);
            let image = decode_dds(&compressed).unwrap();
            assert_eq!(image.red, [expected[0]; 16]);
            assert_eq!(image.green, [expected[1]; 16]);
            assert_eq!(image.blue, [expected[2]; 16]);
            assert_eq!(image.alpha, [expected[3]; 16]);
        }

        #[cfg(not(feature = "dds-bc67"))]
        {
            let mut compressed = dds_header(4, 4, *b"DX10", 0);
            compressed.extend_from_slice(&95u32.to_le_bytes());
            compressed.extend_from_slice(&[0; 16 + 16]);
            assert!(matches!(decode_dds(&compressed), Err(Error::Parse { .. })));
        }

        let mut packed = dds_header(1, 2, [0; 4], 24);
        packed[20..24].copy_from_slice(&4u32.to_le_bytes());
        packed[92..96].copy_from_slice(&0x00ff_0000_u32.to_le_bytes());
        packed[96..100].copy_from_slice(&0x0000_ff00_u32.to_le_bytes());
        packed[100..104].copy_from_slice(&0x0000_00ff_u32.to_le_bytes());
        packed.extend_from_slice(&[3, 2, 1, 6, 5, 4]);
        let image = decode_dds(&packed).unwrap();
        assert_eq!(image.red, [1, 4]);
        assert_eq!(image.green, [2, 5]);
        assert_eq!(image.blue, [3, 6]);

        assert_eq!(blend_rgb565(0xf800, 0, 2, 1, 3), [170, 0, 0]);
    }

    #[test]
    fn stream_dispatch_and_malformed_sizes_return_typed_errors() {
        let mut tga = tga_header(3, 1, 1, 8, 0x20);
        tga.push(77);
        let image = Targa::decode_to_managed_image_with_stream(Box::new(Cursor::new(tga))).unwrap();
        assert_eq!(image.red, [77]);

        let huge = tga_header(2, u16::MAX, u16::MAX, 24, 0);
        assert_eq!(decode_tga(&huge), Err(Error::Argument));
        assert!(matches!(
            decode_tga(&tga_header(10, 1, 1, 24, 0)),
            Err(Error::Parse { .. })
        ));
        let mut oversized_packet = tga_header(10, 1, 1, 24, 0);
        oversized_packet.extend_from_slice(&[0x81, 0, 0, 0]);
        assert!(matches!(
            decode_tga(&oversized_packet),
            Err(Error::Parse { .. })
        ));
        assert!(matches!(decode_dds(b"DDS "), Err(Error::Parse { .. })));
    }

    #[test]
    fn dxt3_and_dxt5_alpha_tables_are_exact() {
        let dxt3 = [0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe];
        let alpha = dxt_alphas(&dxt3, 3).unwrap();
        assert_eq!(&alpha[..4], &[0, 17, 34, 51]);
        assert_eq!(alpha[15], 255);

        let mut dxt5 = [0; 8];
        dxt5[0] = 255;
        dxt5[1] = 0;
        dxt5[2] = 0b0000_0010;
        let alpha = dxt_alphas(&dxt5, 5).unwrap();
        assert_eq!(alpha[0], 218);
        assert_eq!(alpha[1], 255);
    }
}
