use crate::error::{Error, Result};

/// Pixel format of a TEX surface.
///
/// The `BC*` variants are block-compressed; [`TextureFormat::is_block_compressed`] tests for
/// membership.  Convert to/from the raw on-disk `u32` discriminant with [`TextureFormat::from_raw`]
/// and [`TextureFormat::as_raw`].
///
/// `BC7` is recognised but cannot be decompressed by [`crate::tex::bcn::decompress_to_rgba8`].
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TextureFormat {
    /// 8-bit luminance (single channel, maps to red).
    L8,
    /// 8-bit alpha (single channel).
    A8,
    /// 16-bit BGRA 4:4:4:4.
    B4G4R4A4,
    /// 16-bit BGRA 5:5:5:1.
    B5G5R5A1,
    /// 32-bit BGRA 8:8:8:8.
    B8G8R8A8,
    /// 32-bit BGRX 8:8:8:8 (alpha channel ignored).
    B8G8R8X8,
    /// 64-bit RGBA half-float (16 bits per channel).
    R16G16B16A16F,
    /// 128-bit RGBA single-float (32 bits per channel).
    R32G32B32A32F,
    /// BC1 (DXT1) block compression — opaque or 1-bit alpha.
    BC1,
    /// BC2 (DXT3) block compression — explicit 4-bit alpha.
    BC2,
    /// BC3 (DXT5) block compression — interpolated alpha.
    BC3,
    /// BC4 block compression — single channel (red).
    BC4,
    /// BC5 block compression — two channels (red + green).
    BC5,
    /// BC7 block compression — high-quality RGBA.  Not software-decompressible.
    BC7,
    /// A raw discriminant not recognised by this crate.
    Unknown(u32),
}

impl TextureFormat {
    /// Converts the on-disk `u32` format discriminant to a [`TextureFormat`].
    ///
    /// Unknown discriminants are wrapped in [`TextureFormat::Unknown`] rather than returning an
    /// error, preserving the value for round-trip fidelity.
    #[must_use]
    pub fn from_raw(raw: u32) -> Self {
        match raw {
            0x1130 => TextureFormat::L8,
            0x1131 => TextureFormat::A8,
            0x1440 => TextureFormat::B4G4R4A4,
            0x1441 => TextureFormat::B5G5R5A1,
            0x1450 => TextureFormat::B8G8R8A8,
            0x1451 => TextureFormat::B8G8R8X8,
            0x2460 => TextureFormat::R16G16B16A16F,
            0x2470 => TextureFormat::R32G32B32A32F,
            0x3420 => TextureFormat::BC1,
            0x3430 => TextureFormat::BC2,
            0x3431 => TextureFormat::BC3,
            0x6120 => TextureFormat::BC4,
            0x6230 => TextureFormat::BC5,
            0x6432 => TextureFormat::BC7,
            other => TextureFormat::Unknown(other),
        }
    }

    /// Returns the on-disk `u32` discriminant for this format.
    #[must_use]
    pub fn as_raw(self) -> u32 {
        match self {
            TextureFormat::L8 => 0x1130,
            TextureFormat::A8 => 0x1131,
            TextureFormat::B4G4R4A4 => 0x1440,
            TextureFormat::B5G5R5A1 => 0x1441,
            TextureFormat::B8G8R8A8 => 0x1450,
            TextureFormat::B8G8R8X8 => 0x1451,
            TextureFormat::R16G16B16A16F => 0x2460,
            TextureFormat::R32G32B32A32F => 0x2470,
            TextureFormat::BC1 => 0x3420,
            TextureFormat::BC2 => 0x3430,
            TextureFormat::BC3 => 0x3431,
            TextureFormat::BC4 => 0x6120,
            TextureFormat::BC5 => 0x6230,
            TextureFormat::BC7 => 0x6432,
            TextureFormat::Unknown(raw) => raw,
        }
    }

    /// Returns `true` for any `BCn` variant.
    #[must_use]
    pub fn is_block_compressed(self) -> bool {
        matches!(
            self,
            TextureFormat::BC1
                | TextureFormat::BC2
                | TextureFormat::BC3
                | TextureFormat::BC4
                | TextureFormat::BC5
                | TextureFormat::BC7
        )
    }

    /// Returns the number of bytes per 4×4 compressed block, or `None` for linear formats.
    #[must_use]
    pub fn bytes_per_block(self) -> Option<u32> {
        match self {
            TextureFormat::BC1 | TextureFormat::BC4 => Some(8),
            TextureFormat::BC2 | TextureFormat::BC3 | TextureFormat::BC5 | TextureFormat::BC7 => {
                Some(16)
            }
            _ => None,
        }
    }

    /// Returns the bits per pixel for linear (non-block-compressed) formats, or `None` for `BCn`
    /// and unknown formats.
    #[must_use]
    pub fn bits_per_pixel(self) -> Option<u32> {
        match self {
            TextureFormat::L8 | TextureFormat::A8 => Some(8),
            TextureFormat::B4G4R4A4 | TextureFormat::B5G5R5A1 => Some(16),
            TextureFormat::B8G8R8A8 | TextureFormat::B8G8R8X8 => Some(32),
            TextureFormat::R16G16B16A16F => Some(64),
            TextureFormat::R32G32B32A32F => Some(128),
            TextureFormat::BC1
            | TextureFormat::BC2
            | TextureFormat::BC3
            | TextureFormat::BC4
            | TextureFormat::BC5
            | TextureFormat::BC7
            | TextureFormat::Unknown(_) => None,
        }
    }

    /// Computes the byte size of a surface with the given dimensions.
    ///
    /// For `BCn` formats the width and height are rounded up to the nearest 4-texel block boundary.
    /// Returns [`Error::UnsupportedTextureFormat`] for [`TextureFormat::Unknown`].
    pub fn surface_byte_size(self, width: u32, height: u32) -> Result<u64> {
        if let Some(bpb) = self.bytes_per_block() {
            let bw = u64::from(width.div_ceil(4).max(1));
            let bh = u64::from(height.div_ceil(4).max(1));
            let bpb = u64::from(bpb);
            return Ok(bw * bh * bpb);
        }
        if let Some(bpp) = self.bits_per_pixel() {
            let w = u64::from(width);
            let h = u64::from(height);
            let bpp = u64::from(bpp);
            return Ok(w * h * bpp / 8);
        }
        Err(Error::UnsupportedTextureFormat {
            format: self.as_raw(),
        })
    }
}

impl std::fmt::Display for TextureFormat {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            TextureFormat::L8 => f.write_str("L8"),
            TextureFormat::A8 => f.write_str("A8"),
            TextureFormat::B4G4R4A4 => f.write_str("B4G4R4A4"),
            TextureFormat::B5G5R5A1 => f.write_str("B5G5R5A1"),
            TextureFormat::B8G8R8A8 => f.write_str("B8G8R8A8"),
            TextureFormat::B8G8R8X8 => f.write_str("B8G8R8X8"),
            TextureFormat::R16G16B16A16F => f.write_str("R16G16B16A16F"),
            TextureFormat::R32G32B32A32F => f.write_str("R32G32B32A32F"),
            TextureFormat::BC1 => f.write_str("BC1"),
            TextureFormat::BC2 => f.write_str("BC2"),
            TextureFormat::BC3 => f.write_str("BC3"),
            TextureFormat::BC4 => f.write_str("BC4"),
            TextureFormat::BC5 => f.write_str("BC5"),
            TextureFormat::BC7 => f.write_str("BC7"),
            TextureFormat::Unknown(raw) => write!(f, "Unknown({raw:#010x})"),
        }
    }
}

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

    #[test]
    fn from_raw_round_trips_every_known_discriminant() {
        let cases: &[(u32, TextureFormat)] = &[
            (0x1130, TextureFormat::L8),
            (0x1131, TextureFormat::A8),
            (0x1440, TextureFormat::B4G4R4A4),
            (0x1441, TextureFormat::B5G5R5A1),
            (0x1450, TextureFormat::B8G8R8A8),
            (0x1451, TextureFormat::B8G8R8X8),
            (0x2460, TextureFormat::R16G16B16A16F),
            (0x2470, TextureFormat::R32G32B32A32F),
            (0x3420, TextureFormat::BC1),
            (0x3430, TextureFormat::BC2),
            (0x3431, TextureFormat::BC3),
            (0x6120, TextureFormat::BC4),
            (0x6230, TextureFormat::BC5),
            (0x6432, TextureFormat::BC7),
        ];
        for &(raw, expected) in cases {
            assert_eq!(TextureFormat::from_raw(raw), expected, "raw {raw:#06x}");
            assert_eq!(expected.as_raw(), raw, "as_raw {expected:?}");
        }
    }

    #[test]
    fn unknown_round_trips_verbatim() {
        let raw = 0xDEAD_BEEF;
        let fmt = TextureFormat::from_raw(raw);
        assert_eq!(fmt, TextureFormat::Unknown(raw));
        assert_eq!(fmt.as_raw(), raw);
    }

    #[test]
    fn is_block_compressed_covers_bcn() {
        assert!(TextureFormat::BC1.is_block_compressed());
        assert!(TextureFormat::BC2.is_block_compressed());
        assert!(TextureFormat::BC3.is_block_compressed());
        assert!(TextureFormat::BC4.is_block_compressed());
        assert!(TextureFormat::BC5.is_block_compressed());
        assert!(TextureFormat::BC7.is_block_compressed());
        assert!(!TextureFormat::B8G8R8A8.is_block_compressed());
        assert!(!TextureFormat::L8.is_block_compressed());
        assert!(!TextureFormat::Unknown(0x9999).is_block_compressed());
    }

    #[test]
    fn bytes_per_block_matches_format_family() {
        assert_eq!(TextureFormat::BC1.bytes_per_block(), Some(8));
        assert_eq!(TextureFormat::BC4.bytes_per_block(), Some(8));
        assert_eq!(TextureFormat::BC2.bytes_per_block(), Some(16));
        assert_eq!(TextureFormat::BC3.bytes_per_block(), Some(16));
        assert_eq!(TextureFormat::BC5.bytes_per_block(), Some(16));
        assert_eq!(TextureFormat::BC7.bytes_per_block(), Some(16));
        assert_eq!(TextureFormat::B8G8R8A8.bytes_per_block(), None);
    }

    #[test]
    fn bits_per_pixel_matches_format_family() {
        assert_eq!(TextureFormat::L8.bits_per_pixel(), Some(8));
        assert_eq!(TextureFormat::A8.bits_per_pixel(), Some(8));
        assert_eq!(TextureFormat::B4G4R4A4.bits_per_pixel(), Some(16));
        assert_eq!(TextureFormat::B5G5R5A1.bits_per_pixel(), Some(16));
        assert_eq!(TextureFormat::B8G8R8A8.bits_per_pixel(), Some(32));
        assert_eq!(TextureFormat::B8G8R8X8.bits_per_pixel(), Some(32));
        assert_eq!(TextureFormat::R16G16B16A16F.bits_per_pixel(), Some(64));
        assert_eq!(TextureFormat::R32G32B32A32F.bits_per_pixel(), Some(128));
        assert_eq!(TextureFormat::BC1.bits_per_pixel(), None);
    }

    #[test]
    fn surface_byte_size_bcn_uses_block_rounding() {
        assert_eq!(TextureFormat::BC1.surface_byte_size(8, 8).unwrap(), 32);
        assert_eq!(TextureFormat::BC1.surface_byte_size(5, 5).unwrap(), 32);
        assert_eq!(TextureFormat::BC1.surface_byte_size(1, 1).unwrap(), 8);
        assert_eq!(TextureFormat::BC3.surface_byte_size(4, 4).unwrap(), 16);
    }

    #[test]
    fn surface_byte_size_bc4_uses_8_bytes_per_block() {
        assert_eq!(TextureFormat::BC4.surface_byte_size(4, 4).unwrap(), 8);
        assert_eq!(TextureFormat::BC4.surface_byte_size(8, 8).unwrap(), 32);
        assert_eq!(TextureFormat::BC4.surface_byte_size(5, 5).unwrap(), 32);
        assert_eq!(TextureFormat::BC4.surface_byte_size(1, 1).unwrap(), 8);
    }

    #[test]
    fn surface_byte_size_bc5_uses_16_bytes_per_block() {
        assert_eq!(TextureFormat::BC5.surface_byte_size(4, 4).unwrap(), 16);
        assert_eq!(TextureFormat::BC5.surface_byte_size(8, 8).unwrap(), 64);
        assert_eq!(TextureFormat::BC5.surface_byte_size(5, 5).unwrap(), 64);
    }

    #[test]
    fn surface_byte_size_linear_1x1_is_minimum_pixel_size() {
        assert_eq!(TextureFormat::L8.surface_byte_size(1, 1).unwrap(), 1);
        assert_eq!(TextureFormat::A8.surface_byte_size(1, 1).unwrap(), 1);
        assert_eq!(TextureFormat::B4G4R4A4.surface_byte_size(1, 1).unwrap(), 2);
        assert_eq!(TextureFormat::B5G5R5A1.surface_byte_size(1, 1).unwrap(), 2);
        assert_eq!(TextureFormat::B8G8R8A8.surface_byte_size(1, 1).unwrap(), 4);
        assert_eq!(TextureFormat::B8G8R8X8.surface_byte_size(1, 1).unwrap(), 4);
        assert_eq!(
            TextureFormat::R16G16B16A16F
                .surface_byte_size(1, 1)
                .unwrap(),
            8
        );
        assert_eq!(
            TextureFormat::R32G32B32A32F
                .surface_byte_size(1, 1)
                .unwrap(),
            16
        );
    }

    #[test]
    fn surface_byte_size_unknown_format_errors() {
        let err = TextureFormat::Unknown(0x9999)
            .surface_byte_size(8, 8)
            .unwrap_err();
        assert!(matches!(
            err,
            Error::UnsupportedTextureFormat { format: 0x9999 }
        ));
    }

    #[test]
    fn bits_per_pixel_returns_none_for_all_bcn_and_unknown() {
        for fmt in [
            TextureFormat::BC1,
            TextureFormat::BC2,
            TextureFormat::BC3,
            TextureFormat::BC4,
            TextureFormat::BC5,
            TextureFormat::BC7,
            TextureFormat::Unknown(0x1234),
        ] {
            assert!(
                fmt.bits_per_pixel().is_none(),
                "{fmt:?}.bits_per_pixel() should be None"
            );
        }
    }

    #[test]
    fn bytes_per_block_returns_none_for_all_linear_formats() {
        for fmt in [
            TextureFormat::L8,
            TextureFormat::A8,
            TextureFormat::B4G4R4A4,
            TextureFormat::B5G5R5A1,
            TextureFormat::B8G8R8A8,
            TextureFormat::B8G8R8X8,
            TextureFormat::R16G16B16A16F,
            TextureFormat::R32G32B32A32F,
            TextureFormat::Unknown(0x1234),
        ] {
            assert!(
                fmt.bytes_per_block().is_none(),
                "{fmt:?}.bytes_per_block() should be None"
            );
        }
    }

    #[test]
    fn surface_byte_size_bc7_one_block_at_minimum_dimension() {
        assert_eq!(TextureFormat::BC7.surface_byte_size(1, 1).unwrap(), 16);
        assert_eq!(TextureFormat::BC7.surface_byte_size(4, 4).unwrap(), 16);
        assert_eq!(TextureFormat::BC7.surface_byte_size(8, 8).unwrap(), 64);
    }

    #[test]
    fn surface_byte_size_bc2_matches_bc3_and_bc5() {
        assert_eq!(TextureFormat::BC2.surface_byte_size(4, 4).unwrap(), 16);
        assert_eq!(TextureFormat::BC2.surface_byte_size(8, 8).unwrap(), 64);
        assert_eq!(
            TextureFormat::BC2.surface_byte_size(5, 5).unwrap(),
            TextureFormat::BC3.surface_byte_size(5, 5).unwrap(),
            "BC2 and BC3 must agree on a non-block-aligned dimension"
        );
    }

    #[test]
    fn surface_byte_size_non_block_aligned_7x7_rounds_up() {
        assert_eq!(TextureFormat::BC1.surface_byte_size(7, 7).unwrap(), 32);
        assert_eq!(TextureFormat::BC3.surface_byte_size(7, 7).unwrap(), 64);
        assert_eq!(
            TextureFormat::B8G8R8A8.surface_byte_size(7, 7).unwrap(),
            196
        );
    }

    #[test]
    fn surface_byte_size_256x256_bc1_is_32_kib() {
        assert_eq!(
            TextureFormat::BC1.surface_byte_size(256, 256).unwrap(),
            32_768
        );
    }

    #[test]
    fn surface_byte_size_linear_property_bpp_times_w_times_h_div_8() {
        let linear_formats = [
            TextureFormat::L8,
            TextureFormat::A8,
            TextureFormat::B4G4R4A4,
            TextureFormat::B5G5R5A1,
            TextureFormat::B8G8R8A8,
            TextureFormat::B8G8R8X8,
            TextureFormat::R16G16B16A16F,
            TextureFormat::R32G32B32A32F,
        ];
        for fmt in linear_formats {
            let bpp = u64::from(fmt.bits_per_pixel().expect("linear format has bpp"));
            for &(w, h) in &[(1u32, 1u32), (4, 4), (8, 8), (5, 5), (7, 3)] {
                let expected = bpp * u64::from(w) * u64::from(h) / 8;
                assert_eq!(
                    fmt.surface_byte_size(w, h).unwrap(),
                    expected,
                    "{fmt:?} surface_byte_size({w}, {h})"
                );
            }
        }
    }
}
