//! Native implementation of the pinned `SkiaTextureCodec` behavior.

use crate::Error;
use crate::backend::{SKAlphaType, SKBitmap, SKColorType};
#[cfg(any(feature = "rust-skia", feature = "skia"))]
use libremetaverse_imaging::{DEFAULT_MAX_ENCODED_BYTES, DEFAULT_MAX_PIXELS};
use libremetaverse_imaging::{ManagedImage, ManagedImageImageChannels};
use libremetaverse_types::compat::ReadWrite;
#[cfg(any(feature = "rust-skia", feature = "skia"))]
use std::io::Read;

/// Optional Skia-backed decoder and format-neutral bitmap converter.
#[derive(Clone, Copy, Debug, Default)]
pub struct SkiaTextureCodec;

/// Pure-Rust alternative to [`SkiaTextureCodec`] with the same image boundary.
///
/// Enable the `rust-skia` feature to decode the common Skia codec formats
/// without a native library, build script, system package lookup, or download.
#[derive(Clone, Copy, Debug, Default)]
pub struct RustSkiaTextureCodec;

impl SkiaTextureCodec {
    /// Creates the stateless codec adapter.
    ///
    /// # Errors
    ///
    /// This fixed compatibility constructor cannot fail.
    pub const fn new() -> Result<Self, Error> {
        Ok(Self)
    }

    /// Decodes a bounded compressed-image stream through Skia.
    ///
    /// # Errors
    ///
    /// With the `skia` feature, returns a typed argument, parse, or operation
    /// error for oversized or malformed input and unsupported native output.
    /// Without the feature it returns [`Error::InvalidOperation`].
    pub fn decode(&self, mut stream: Box<dyn ReadWrite + Send>) -> Result<ManagedImage, Error> {
        #[cfg(feature = "skia")]
        {
            let encoded = read_bounded(&mut stream)?;
            decode_with_skia(&encoded)
        }
        #[cfg(not(feature = "skia"))]
        {
            let _ = &mut stream;
            Err(Error::InvalidOperation)
        }
    }

    /// Converts an owned mapped bitmap using the exact reference channel rules.
    ///
    /// # Errors
    ///
    /// Returns a typed error for invalid dimensions, storage, or an unsupported
    /// fallback byte width.
    #[allow(clippy::needless_pass_by_value)] // fixed mapped C# signature
    pub fn to_managed_image(bitmap: SKBitmap) -> Result<ManagedImage, Error> {
        bitmap_to_managed(&bitmap)
    }
}

impl RustSkiaTextureCodec {
    /// Creates the stateless pure-Rust codec adapter.
    ///
    /// # Errors
    ///
    /// This fixed compatibility constructor cannot fail.
    pub const fn new() -> Result<Self, Error> {
        Ok(Self)
    }

    /// Decodes a bounded stream with Rust-only BMP, GIF, ICO, JPEG, PNG,
    /// WBMP, and WebP codecs.
    ///
    /// # Errors
    ///
    /// With `rust-skia`, returns [`Error::Argument`] for an input or decoded
    /// canvas above the documented limits and [`Error::InvalidOperation`] for
    /// malformed or unsupported images. Without the feature it returns
    /// [`Error::InvalidOperation`].
    pub fn decode(&self, mut stream: Box<dyn ReadWrite + Send>) -> Result<ManagedImage, Error> {
        #[cfg(feature = "rust-skia")]
        {
            let encoded = read_bounded(&mut stream)?;
            decode_with_rust_skia(&encoded)
        }
        #[cfg(not(feature = "rust-skia"))]
        {
            let _ = &mut stream;
            Err(Error::InvalidOperation)
        }
    }
}

#[cfg(any(feature = "rust-skia", feature = "skia"))]
fn read_bounded(stream: &mut (dyn ReadWrite + Send)) -> Result<Vec<u8>, Error> {
    let mut encoded = Vec::new();
    stream
        .take((DEFAULT_MAX_ENCODED_BYTES + 1) as u64)
        .read_to_end(&mut encoded)
        .map_err(|_| Error::InvalidOperation)?;
    if encoded.is_empty() {
        return Err(Error::InvalidOperation);
    }
    if encoded.len() > DEFAULT_MAX_ENCODED_BYTES {
        return Err(Error::Argument);
    }
    Ok(encoded)
}

// Keeping the format branches together makes comparison with the pinned C#
// color switch auditable and prevents subtly different indexing paths.
#[allow(clippy::too_many_lines)]
fn bitmap_to_managed(bitmap: &SKBitmap) -> Result<ManagedImage, Error> {
    let width = usize::try_from(bitmap.width()).map_err(|_| Error::Argument)?;
    let height = usize::try_from(bitmap.height()).map_err(|_| Error::Argument)?;
    let reference_step = bitmap.row_bytes() / width.max(1);
    let channels = match bitmap.color_type() {
        SKColorType::Rgb565 => ManagedImageImageChannels::COLOR,
        SKColorType::Bgra8888
        | SKColorType::Rgba8888
        | SKColorType::Rgba1010102
        | SKColorType::Bgra1010102 => {
            ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA
        }
        SKColorType::Gray8 => ManagedImageImageChannels::GRAY,
        SKColorType::Alpha8 => ManagedImageImageChannels::ALPHA,
        SKColorType::Other { .. } if reference_step == 1 => ManagedImageImageChannels::GRAY,
        SKColorType::Other { .. } if reference_step == 3 => ManagedImageImageChannels::COLOR,
        SKColorType::Other { .. } if reference_step == 4 => {
            ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA
        }
        SKColorType::Other { .. } => return Err(Error::InvalidOperation),
    };
    let mut image = ManagedImage::new(bitmap.width(), bitmap.height(), channels)?;
    for y in 0..height {
        let row = y.checked_mul(bitmap.row_bytes()).ok_or(Error::Argument)?;
        for x in 0..width {
            let pixel = y
                .checked_mul(width)
                .and_then(|offset| offset.checked_add(x))
                .ok_or(Error::Argument)?;
            match bitmap.color_type() {
                SKColorType::Rgb565 => {
                    let offset = checked_offset(row, x, 2, bitmap.pixels().len())?;
                    let packed =
                        u16::from_le_bytes([bitmap.pixels()[offset], bitmap.pixels()[offset + 1]]);
                    image.red[pixel] = scale_bits(u32::from((packed >> 11) & 0x1f), 31);
                    image.green[pixel] = scale_bits(u32::from((packed >> 5) & 0x3f), 63);
                    image.blue[pixel] = scale_bits(u32::from(packed & 0x1f), 31);
                }
                SKColorType::Bgra8888 | SKColorType::Rgba8888 => {
                    let offset = checked_offset(row, x, reference_step, bitmap.pixels().len())?;
                    let source = &bitmap.pixels()[offset..offset + 4];
                    let (red, green, blue, alpha) = if bitmap.color_type() == SKColorType::Rgba8888
                    {
                        (source[0], source[1], source[2], source[3])
                    } else {
                        (source[2], source[1], source[0], source[3])
                    };
                    let (red, green, blue) =
                        normalize_8bit_alpha(red, green, blue, alpha, bitmap.alpha_type());
                    image.red[pixel] = red;
                    image.green[pixel] = green;
                    image.blue[pixel] = blue;
                    image.alpha[pixel] = alpha;
                }
                SKColorType::Rgba1010102 | SKColorType::Bgra1010102 => {
                    let offset = checked_offset(row, x, 4, bitmap.pixels().len())?;
                    let packed = u32::from_le_bytes(
                        bitmap.pixels()[offset..offset + 4]
                            .try_into()
                            .map_err(|_| Error::Argument)?,
                    );
                    let first = packed & 0x3ff;
                    let green = (packed >> 10) & 0x3ff;
                    let third = (packed >> 20) & 0x3ff;
                    let alpha = (packed >> 30) & 0x3;
                    let (red, blue) = if bitmap.color_type() == SKColorType::Rgba1010102 {
                        (first, third)
                    } else {
                        (third, first)
                    };
                    let (red, green, blue) =
                        normalize_10bit_alpha(red, green, blue, alpha, bitmap.alpha_type());
                    image.red[pixel] = scale_bits(red, 1023);
                    image.green[pixel] = scale_bits(green, 1023);
                    image.blue[pixel] = scale_bits(blue, 1023);
                    image.alpha[pixel] = u8::try_from(alpha * 85).map_err(|_| Error::Argument)?;
                }
                SKColorType::Gray8 => {
                    let offset = checked_offset(row, x, reference_step, bitmap.pixels().len())?;
                    image.red[pixel] = bitmap.pixels()[offset];
                }
                SKColorType::Alpha8 => {
                    let offset = checked_offset(row, x, reference_step, bitmap.pixels().len())?;
                    image.alpha[pixel] = bitmap.pixels()[offset];
                }
                SKColorType::Other { .. } => {
                    let offset = checked_offset(row, x, reference_step, bitmap.pixels().len())?;
                    match reference_step {
                        4 => {
                            let alpha = bitmap.pixels()[offset + 3];
                            let (red, green, blue) = normalize_8bit_alpha(
                                bitmap.pixels()[offset + 2],
                                bitmap.pixels()[offset + 1],
                                bitmap.pixels()[offset],
                                alpha,
                                bitmap.alpha_type(),
                            );
                            image.red[pixel] = red;
                            image.green[pixel] = green;
                            image.blue[pixel] = blue;
                            image.alpha[pixel] = alpha;
                        }
                        3 => {
                            image.blue[pixel] = bitmap.pixels()[offset];
                            image.green[pixel] = bitmap.pixels()[offset + 1];
                            image.red[pixel] = bitmap.pixels()[offset + 2];
                        }
                        1 => image.red[pixel] = bitmap.pixels()[offset],
                        _ => return Err(Error::InvalidOperation),
                    }
                }
            }
        }
    }
    Ok(image)
}

fn checked_offset(row: usize, x: usize, step: usize, length: usize) -> Result<usize, Error> {
    let offset = x
        .checked_mul(step)
        .and_then(|offset| row.checked_add(offset))
        .ok_or(Error::Argument)?;
    offset
        .checked_add(step)
        .filter(|end| *end <= length)
        .map(|_| offset)
        .ok_or(Error::Argument)
}

fn scale_bits(value: u32, maximum: u32) -> u8 {
    u8::try_from((value * 255 + maximum / 2) / maximum).unwrap_or(u8::MAX)
}

fn normalize_8bit_alpha(
    red: u8,
    green: u8,
    blue: u8,
    alpha: u8,
    alpha_type: SKAlphaType,
) -> (u8, u8, u8) {
    if alpha_type != SKAlphaType::Premul || alpha == u8::MAX {
        return (red, green, blue);
    }
    if alpha == 0 {
        return (0, 0, 0);
    }
    let normalize = |value: u8| {
        u8::try_from((u32::from(value) * 255 + u32::from(alpha) / 2) / u32::from(alpha))
            .unwrap_or(u8::MAX)
    };
    (normalize(red), normalize(green), normalize(blue))
}

fn normalize_10bit_alpha(
    red: u32,
    green: u32,
    blue: u32,
    alpha: u32,
    alpha_type: SKAlphaType,
) -> (u32, u32, u32) {
    if alpha_type != SKAlphaType::Premul || alpha == 3 {
        return (red, green, blue);
    }
    if alpha == 0 {
        return (0, 0, 0);
    }
    let normalize = |value: u32| (value * 3 + alpha / 2) / alpha;
    (normalize(red), normalize(green), normalize(blue))
}

#[cfg(feature = "rust-skia")]
fn decode_with_rust_skia(encoded: &[u8]) -> Result<ManagedImage, Error> {
    use skia_rs_codec::{ImageDecoder, ImageFormat};

    let format = if wbmp_dimensions(encoded).is_ok() {
        ImageFormat::Wbmp
    } else {
        ImageFormat::from_magic(encoded)
    };
    if format == ImageFormat::WebP {
        return decode_webp_with_rust(encoded);
    }
    if !matches!(
        format,
        ImageFormat::Bmp
            | ImageFormat::Gif
            | ImageFormat::Ico
            | ImageFormat::Jpeg
            | ImageFormat::Png
            | ImageFormat::Wbmp
    ) {
        return Err(Error::InvalidOperation);
    }

    let (width, height) = match format {
        ImageFormat::Gif => gif_dimensions(encoded)?,
        ImageFormat::Ico => ico_dimensions(encoded)?,
        ImageFormat::Wbmp => wbmp_dimensions(encoded)?,
        _ => skia_rs_codec::get_image_dimensions(encoded).map_err(|_| Error::InvalidOperation)?,
    };
    validate_dimensions(width, height)?;

    let decoded = if format == ImageFormat::Wbmp {
        skia_rs_codec::WbmpDecoder::new().decode_bytes(encoded)
    } else {
        skia_rs_codec::decode_image(encoded)
    }
    .map_err(|_| Error::InvalidOperation)?;
    validate_dimensions(decoded.width(), decoded.height())?;
    if decoded.width() != width || decoded.height() != height {
        return Err(Error::InvalidOperation);
    }
    let width_usize = usize::try_from(width).map_err(|_| Error::Argument)?;
    if decoded.row_bytes() != width_usize.checked_mul(4).ok_or(Error::Argument)? {
        return Err(Error::InvalidOperation);
    }
    let source_pixels = decoded.peek_pixels().ok_or(Error::InvalidOperation)?;
    let grayscale = match format {
        ImageFormat::Jpeg => jpeg_is_grayscale(encoded)?,
        ImageFormat::Png => encoded.get(25) == Some(&0),
        ImageFormat::Wbmp => true,
        _ => false,
    };
    let (row_bytes, color_type, alpha_type, pixels) = if grayscale {
        let pixel_count = width_usize
            .checked_mul(usize::try_from(height).map_err(|_| Error::Argument)?)
            .ok_or(Error::Argument)?;
        let mut gray = Vec::new();
        gray.try_reserve_exact(pixel_count)
            .map_err(|_| Error::InvalidOperation)?;
        gray.extend(source_pixels.chunks_exact(4).map(|rgba| rgba[0]));
        if gray.len() != pixel_count {
            return Err(Error::InvalidOperation);
        }
        (width_usize, SKColorType::Gray8, SKAlphaType::Opaque, gray)
    } else {
        let mut rgba = Vec::new();
        rgba.try_reserve_exact(source_pixels.len())
            .map_err(|_| Error::InvalidOperation)?;
        rgba.extend_from_slice(source_pixels);
        (
            decoded.row_bytes(),
            SKColorType::Rgba8888,
            if decoded.is_opaque() {
                SKAlphaType::Opaque
            } else {
                SKAlphaType::Unpremul
            },
            rgba,
        )
    };
    let bitmap = SKBitmap::new(width, height, row_bytes, color_type, alpha_type, pixels)?;
    bitmap_to_managed(&bitmap)
}

#[cfg(feature = "rust-skia")]
fn jpeg_is_grayscale(encoded: &[u8]) -> Result<bool, Error> {
    let mut offset = 2;
    while offset + 1 < encoded.len() {
        if encoded[offset] != 0xff {
            offset += 1;
            continue;
        }
        let mut marker_offset = offset + 1;
        while encoded.get(marker_offset) == Some(&0xff) {
            marker_offset += 1;
        }
        let marker = *encoded.get(marker_offset).ok_or(Error::InvalidOperation)?;
        if marker == 0 || marker == 1 || (0xd0..=0xd9).contains(&marker) {
            offset = marker_offset + 1;
            continue;
        }
        let length_bytes = encoded
            .get(marker_offset + 1..marker_offset + 3)
            .ok_or(Error::InvalidOperation)?;
        let length = usize::from(u16::from_be_bytes(
            length_bytes
                .try_into()
                .map_err(|_| Error::InvalidOperation)?,
        ));
        if matches!(
            marker,
            0xc0..=0xc3 | 0xc5..=0xc7 | 0xc9..=0xcb | 0xcd..=0xcf
        ) {
            let components = *encoded
                .get(marker_offset + 8)
                .ok_or(Error::InvalidOperation)?;
            return match components {
                1 => Ok(true),
                3 => Ok(false),
                _ => Err(Error::InvalidOperation),
            };
        }
        if length < 2 {
            return Err(Error::InvalidOperation);
        }
        offset = marker_offset
            .checked_add(1)
            .and_then(|offset| offset.checked_add(length))
            .ok_or(Error::InvalidOperation)?;
    }
    Err(Error::InvalidOperation)
}

#[cfg(feature = "rust-skia")]
fn gif_dimensions(encoded: &[u8]) -> Result<(i32, i32), Error> {
    if encoded.len() < 13 || !(encoded.starts_with(b"GIF87a") || encoded.starts_with(b"GIF89a")) {
        return Err(Error::InvalidOperation);
    }
    let canvas = (
        i32::from(u16::from_le_bytes([encoded[6], encoded[7]])),
        i32::from(u16::from_le_bytes([encoded[8], encoded[9]])),
    );
    validate_dimensions(canvas.0, canvas.1)?;

    let global_table_bytes = if encoded[10] & 0x80 == 0 {
        0
    } else {
        3_usize
            .checked_mul(1_usize << (usize::from(encoded[10] & 0x07) + 1))
            .ok_or(Error::InvalidOperation)?
    };
    let mut offset = 13_usize
        .checked_add(global_table_bytes)
        .filter(|offset| *offset <= encoded.len())
        .ok_or(Error::InvalidOperation)?;
    loop {
        let marker = *encoded.get(offset).ok_or(Error::InvalidOperation)?;
        offset += 1;
        match marker {
            0x2c => {
                let end = offset.checked_add(9).ok_or(Error::InvalidOperation)?;
                let descriptor = encoded.get(offset..end).ok_or(Error::InvalidOperation)?;
                let frame_width = i32::from(u16::from_le_bytes([descriptor[4], descriptor[5]]));
                let frame_height = i32::from(u16::from_le_bytes([descriptor[6], descriptor[7]]));
                validate_dimensions(frame_width, frame_height)?;
                return Ok(canvas);
            }
            0x21 => {
                offset = offset.checked_add(1).ok_or(Error::InvalidOperation)?;
                loop {
                    let length = *encoded.get(offset).ok_or(Error::InvalidOperation)?;
                    offset += 1;
                    if length == 0 {
                        break;
                    }
                    offset = offset
                        .checked_add(usize::from(length))
                        .filter(|offset| *offset <= encoded.len())
                        .ok_or(Error::InvalidOperation)?;
                }
            }
            _ => return Err(Error::InvalidOperation),
        }
    }
}

#[cfg(feature = "rust-skia")]
fn wbmp_dimensions(encoded: &[u8]) -> Result<(i32, i32), Error> {
    if encoded.get(..2) != Some(&[0, 0]) {
        return Err(Error::InvalidOperation);
    }
    let mut offset = 2;
    let width =
        i32::try_from(read_wbmp_integer(encoded, &mut offset)?).map_err(|_| Error::Argument)?;
    let height =
        i32::try_from(read_wbmp_integer(encoded, &mut offset)?).map_err(|_| Error::Argument)?;
    validate_dimensions(width, height)?;
    Ok((width, height))
}

#[cfg(feature = "rust-skia")]
fn read_wbmp_integer(encoded: &[u8], offset: &mut usize) -> Result<u32, Error> {
    let mut value = 0_u32;
    for _ in 0..5 {
        let byte = *encoded.get(*offset).ok_or(Error::InvalidOperation)?;
        *offset += 1;
        value = value
            .checked_shl(7)
            .and_then(|value| value.checked_add(u32::from(byte & 0x7f)))
            .ok_or(Error::InvalidOperation)?;
        if byte & 0x80 == 0 {
            return Ok(value);
        }
    }
    Err(Error::InvalidOperation)
}

#[cfg(feature = "rust-skia")]
fn ico_dimensions(encoded: &[u8]) -> Result<(i32, i32), Error> {
    if encoded.len() < 6 || encoded.get(..4) != Some(&[0, 0, 1, 0]) {
        return Err(Error::InvalidOperation);
    }
    let count = usize::from(u16::from_le_bytes([encoded[4], encoded[5]]));
    let mut best: Option<(u32, usize, usize)> = None;
    for index in 0..count {
        let offset = 6_usize
            .checked_add(index.checked_mul(16).ok_or(Error::InvalidOperation)?)
            .ok_or(Error::InvalidOperation)?;
        let end = offset.checked_add(16).ok_or(Error::InvalidOperation)?;
        let entry = encoded.get(offset..end).ok_or(Error::InvalidOperation)?;
        let width = if entry[0] == 0 {
            256
        } else {
            u32::from(entry[0])
        };
        let height = if entry[1] == 0 {
            256
        } else {
            u32::from(entry[1])
        };
        let area = width.checked_mul(height).ok_or(Error::Argument)?;
        let length = usize::try_from(u32::from_le_bytes(
            entry[8..12]
                .try_into()
                .map_err(|_| Error::InvalidOperation)?,
        ))
        .map_err(|_| Error::Argument)?;
        let image_offset = usize::try_from(u32::from_le_bytes(
            entry[12..16]
                .try_into()
                .map_err(|_| Error::InvalidOperation)?,
        ))
        .map_err(|_| Error::Argument)?;
        if best.is_none_or(|(best_area, _, _)| area > best_area) {
            best = Some((area, image_offset, length));
        }
    }
    let (_, offset, length) = best.ok_or(Error::InvalidOperation)?;
    let end = offset.checked_add(length).ok_or(Error::Argument)?;
    let image = encoded
        .get(offset..end)
        .filter(|image| !image.is_empty())
        .ok_or(Error::InvalidOperation)?;
    if image.starts_with(&[0x89, b'P', b'N', b'G']) {
        return skia_rs_codec::get_image_dimensions(image).map_err(|_| Error::InvalidOperation);
    }
    if image.len() < 40 {
        return Err(Error::InvalidOperation);
    }
    let header_size = u32::from_le_bytes(
        image[0..4]
            .try_into()
            .map_err(|_| Error::InvalidOperation)?,
    );
    if header_size < 40 {
        return Err(Error::InvalidOperation);
    }
    let width = i32::from_le_bytes(
        image[4..8]
            .try_into()
            .map_err(|_| Error::InvalidOperation)?,
    );
    let stored_height = i32::from_le_bytes(
        image[8..12]
            .try_into()
            .map_err(|_| Error::InvalidOperation)?,
    );
    if stored_height <= 0 || stored_height % 2 != 0 {
        return Err(Error::InvalidOperation);
    }
    Ok((width, stored_height / 2))
}

#[cfg(feature = "rust-skia")]
fn validate_dimensions(width: i32, height: i32) -> Result<(), Error> {
    let width = usize::try_from(width).map_err(|_| Error::Argument)?;
    let height = usize::try_from(height).map_err(|_| Error::Argument)?;
    width
        .checked_mul(height)
        .filter(|pixels| *pixels > 0 && *pixels <= DEFAULT_MAX_PIXELS)
        .map(|_| ())
        .ok_or(Error::Argument)
}

#[cfg(feature = "rust-skia")]
fn decode_webp_with_rust(encoded: &[u8]) -> Result<ManagedImage, Error> {
    use std::io::{BufReader, Cursor};

    let reader = BufReader::new(Cursor::new(encoded));
    let mut decoder = image_webp::WebPDecoder::new(reader).map_err(|_| Error::InvalidOperation)?;
    let (width, height) = decoder.dimensions();
    let animated = decoder.is_animated();
    let width_i32 = i32::try_from(width).map_err(|_| Error::Argument)?;
    let height_i32 = i32::try_from(height).map_err(|_| Error::Argument)?;
    validate_dimensions(width_i32, height_i32)?;
    decoder.set_memory_limit(DEFAULT_MAX_PIXELS * 16);

    let output_size = decoder.output_buffer_size().ok_or(Error::Argument)?;
    if output_size > DEFAULT_MAX_PIXELS * 4 {
        return Err(Error::Argument);
    }
    let mut packed_pixels = Vec::new();
    packed_pixels
        .try_reserve_exact(output_size)
        .map_err(|_| Error::InvalidOperation)?;
    packed_pixels.resize(output_size, 0);
    decoder
        .read_image(&mut packed_pixels)
        .map_err(|_| Error::InvalidOperation)?;

    let has_alpha = decoder.has_alpha();
    // image-webp 0.2 uses a divide-by-256 blend fast path for animated
    // canvases. Its fully opaque nonzero samples are consequently one below
    // Skia/libwebp's divide-by-255 result. Correct that bounded first-frame
    // canvas here; partially transparent samples already use the same rounding
    // as the compatibility backend.
    if animated {
        if has_alpha {
            for rgba in packed_pixels.chunks_exact_mut(4) {
                if rgba[3] == u8::MAX {
                    for channel in &mut rgba[..3] {
                        if *channel != 0 {
                            *channel = channel.saturating_add(1);
                        }
                    }
                }
            }
        } else {
            for channel in &mut packed_pixels {
                if *channel != 0 {
                    *channel = channel.saturating_add(1);
                }
            }
        }
    }
    let pixels = if has_alpha {
        packed_pixels
    } else {
        let pixel_count = usize::try_from(width)
            .ok()
            .and_then(|width| {
                usize::try_from(height)
                    .ok()
                    .and_then(|height| width.checked_mul(height))
            })
            .ok_or(Error::Argument)?;
        let rgba_size = pixel_count.checked_mul(4).ok_or(Error::Argument)?;
        let mut rgba = Vec::new();
        rgba.try_reserve_exact(rgba_size)
            .map_err(|_| Error::InvalidOperation)?;
        for rgb in packed_pixels.chunks_exact(3) {
            rgba.extend_from_slice(&[rgb[0], rgb[1], rgb[2], u8::MAX]);
        }
        if rgba.len() != rgba_size {
            return Err(Error::InvalidOperation);
        }
        rgba
    };
    let row_bytes = usize::try_from(width)
        .map_err(|_| Error::Argument)?
        .checked_mul(4)
        .ok_or(Error::Argument)?;
    let bitmap = SKBitmap::new(
        width_i32,
        height_i32,
        row_bytes,
        SKColorType::Rgba8888,
        if has_alpha {
            SKAlphaType::Unpremul
        } else {
            SKAlphaType::Opaque
        },
        pixels,
    )?;
    bitmap_to_managed(&bitmap)
}

#[cfg(feature = "skia")]
fn decode_with_skia(encoded: &[u8]) -> Result<ManagedImage, Error> {
    use skia_safe::Data;
    use skia_safe::codec::{Codec, Options, Result as CodecResult};

    let mut codec = Codec::from_data(Data::new_copy(encoded)).ok_or(Error::InvalidOperation)?;
    let source_info = codec.info();
    let width = usize::try_from(source_info.width()).map_err(|_| Error::Argument)?;
    let height = usize::try_from(source_info.height()).map_err(|_| Error::Argument)?;
    width
        .checked_mul(height)
        .filter(|pixels| *pixels > 0 && *pixels <= DEFAULT_MAX_PIXELS)
        .ok_or(Error::Argument)?;
    // Match SKBitmap.Decode: decode into Skia's native alpha representation,
    // then normalize premultiplied channels at the abstraction boundary.
    let target_info = source_info.clone();
    let row_bytes = target_info.min_row_bytes();
    let length = target_info.compute_byte_size(row_bytes);
    if length == usize::MAX || length > DEFAULT_MAX_PIXELS * 16 {
        return Err(Error::Argument);
    }
    let mut pixels = Vec::new();
    pixels
        .try_reserve_exact(length)
        .map_err(|_| Error::InvalidOperation)?;
    pixels.resize(length, 0);
    let result = codec.get_pixels_with_options(
        &target_info,
        &mut pixels,
        row_bytes,
        Some(&Options {
            max_decode_memory: Some(DEFAULT_MAX_PIXELS * 16),
            ..Options::default()
        }),
    );
    if result != CodecResult::Success {
        return Err(Error::InvalidOperation);
    }
    let bitmap = SKBitmap::new(
        source_info.width(),
        source_info.height(),
        row_bytes,
        map_color_type(target_info.color_type(), target_info.bytes_per_pixel()),
        map_alpha_type(target_info.alpha_type()),
        pixels,
    )?;
    bitmap_to_managed(&bitmap)
}

#[cfg(feature = "skia")]
const fn map_color_type(color: skia_safe::ColorType, bytes_per_pixel: usize) -> SKColorType {
    use skia_safe::ColorType;
    match color {
        ColorType::RGB565 => SKColorType::Rgb565,
        ColorType::BGRA8888 => SKColorType::Bgra8888,
        ColorType::RGBA8888 => SKColorType::Rgba8888,
        ColorType::RGBA1010102 => SKColorType::Rgba1010102,
        ColorType::BGRA1010102 => SKColorType::Bgra1010102,
        ColorType::Gray8 => SKColorType::Gray8,
        ColorType::Alpha8 => SKColorType::Alpha8,
        _ => SKColorType::Other { bytes_per_pixel },
    }
}

#[cfg(feature = "skia")]
const fn map_alpha_type(alpha: skia_safe::AlphaType) -> SKAlphaType {
    use skia_safe::AlphaType;
    match alpha {
        AlphaType::Opaque => SKAlphaType::Opaque,
        AlphaType::Premul => SKAlphaType::Premul,
        AlphaType::Unpremul => SKAlphaType::Unpremul,
        AlphaType::Unknown => SKAlphaType::Unknown,
    }
}

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

    #[test]
    fn mapped_bitmap_formats_preserve_reference_channel_rules() {
        let rgba = SKBitmap::new(
            2,
            1,
            8,
            SKColorType::Rgba8888,
            SKAlphaType::Unpremul,
            vec![1, 2, 3, 4, 5, 6, 7, 8],
        )
        .unwrap();
        let image = SkiaTextureCodec::to_managed_image(rgba).unwrap();
        assert_eq!(image.red, [1, 5]);
        assert_eq!(image.green, [2, 6]);
        assert_eq!(image.blue, [3, 7]);
        assert_eq!(image.alpha, [4, 8]);

        let rgb565 = SKBitmap::new(
            3,
            1,
            6,
            SKColorType::Rgb565,
            SKAlphaType::Opaque,
            vec![0x00, 0xf8, 0xe0, 0x07, 0x1f, 0x00],
        )
        .unwrap();
        let image = SkiaTextureCodec::to_managed_image(rgb565).unwrap();
        assert_eq!(image.red, [255, 0, 0]);
        assert_eq!(image.green, [0, 255, 0]);
        assert_eq!(image.blue, [0, 0, 255]);

        // The reference fallback derives its byte width from rowBytes / width,
        // even when the declared native format has a different packed width.
        let padded_unknown = SKBitmap::new(
            2,
            1,
            8,
            SKColorType::Other { bytes_per_pixel: 3 },
            SKAlphaType::Unpremul,
            vec![3, 2, 1, 4, 7, 6, 5, 8],
        )
        .unwrap();
        let image = SkiaTextureCodec::to_managed_image(padded_unknown).unwrap();
        assert_eq!(
            image.channels,
            ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA
        );
        assert_eq!(image.red, [1, 5]);
        assert_eq!(image.green, [2, 6]);
        assert_eq!(image.blue, [3, 7]);
        assert_eq!(image.alpha, [4, 8]);
    }

    #[test]
    fn grayscale_alpha_ten_bit_and_premul_are_converted() {
        let gray = SKBitmap::new(
            2,
            1,
            2,
            SKColorType::Gray8,
            SKAlphaType::Opaque,
            vec![11, 239],
        )
        .unwrap();
        let gray = SkiaTextureCodec::to_managed_image(gray).unwrap();
        assert_eq!(gray.channels, ManagedImageImageChannels::GRAY);
        assert_eq!(gray.red, [11, 239]);

        let premul = SKBitmap::new(
            1,
            1,
            4,
            SKColorType::Bgra8888,
            SKAlphaType::Premul,
            vec![25, 50, 100, 128],
        )
        .unwrap();
        let premul = SkiaTextureCodec::to_managed_image(premul).unwrap();
        assert_eq!(premul.red, [199]);
        assert_eq!(premul.green, [100]);
        assert_eq!(premul.blue, [50]);
        assert_eq!(premul.alpha, [128]);

        let transparent_premul = SKBitmap::new(
            1,
            1,
            4,
            SKColorType::Rgba8888,
            SKAlphaType::Premul,
            vec![100, 50, 25, 0],
        )
        .unwrap();
        let transparent_premul = SkiaTextureCodec::to_managed_image(transparent_premul).unwrap();
        assert_eq!(transparent_premul.red, [0]);
        assert_eq!(transparent_premul.green, [0]);
        assert_eq!(transparent_premul.blue, [0]);
        assert_eq!(transparent_premul.alpha, [0]);

        let packed = 1023_u32 | (512 << 10) | (1 << 20) | (3 << 30);
        let ten_bit = SKBitmap::new(
            1,
            1,
            4,
            SKColorType::Rgba1010102,
            SKAlphaType::Unpremul,
            packed.to_le_bytes().to_vec(),
        )
        .unwrap();
        let ten_bit = SkiaTextureCodec::to_managed_image(ten_bit).unwrap();
        assert_eq!(ten_bit.red, [255]);
        assert_eq!(ten_bit.green, [128]);
        assert_eq!(ten_bit.blue, [0]);
        assert_eq!(ten_bit.alpha, [255]);

        let packed = 1023_u32 | (512 << 10) | (1 << 20) | (2 << 30);
        let ten_bit = SKBitmap::new(
            1,
            1,
            4,
            SKColorType::Bgra1010102,
            SKAlphaType::Unpremul,
            packed.to_le_bytes().to_vec(),
        )
        .unwrap();
        let ten_bit = SkiaTextureCodec::to_managed_image(ten_bit).unwrap();
        assert_eq!(ten_bit.red, [0]);
        assert_eq!(ten_bit.green, [128]);
        assert_eq!(ten_bit.blue, [255]);
        assert_eq!(ten_bit.alpha, [170]);
    }

    #[test]
    fn alpha_and_reference_fallback_layouts_are_converted_or_rejected() {
        let alpha = SKBitmap::new(
            2,
            1,
            2,
            SKColorType::Alpha8,
            SKAlphaType::Unpremul,
            vec![17, 241],
        )
        .unwrap();
        let alpha = SkiaTextureCodec::to_managed_image(alpha).unwrap();
        assert_eq!(alpha.channels, ManagedImageImageChannels::ALPHA);
        assert_eq!(alpha.alpha, [17, 241]);

        let bgr = SKBitmap::new(
            2,
            1,
            6,
            SKColorType::Other { bytes_per_pixel: 3 },
            SKAlphaType::Opaque,
            vec![3, 2, 1, 6, 5, 4],
        )
        .unwrap();
        let bgr = SkiaTextureCodec::to_managed_image(bgr).unwrap();
        assert_eq!(bgr.channels, ManagedImageImageChannels::COLOR);
        assert_eq!(bgr.red, [1, 4]);
        assert_eq!(bgr.green, [2, 5]);
        assert_eq!(bgr.blue, [3, 6]);

        let gray = SKBitmap::new(
            2,
            1,
            2,
            SKColorType::Other { bytes_per_pixel: 1 },
            SKAlphaType::Opaque,
            vec![23, 229],
        )
        .unwrap();
        let gray = SkiaTextureCodec::to_managed_image(gray).unwrap();
        assert_eq!(gray.channels, ManagedImageImageChannels::GRAY);
        assert_eq!(gray.red, [23, 229]);

        let unsupported = SKBitmap::new(
            1,
            1,
            2,
            SKColorType::Other { bytes_per_pixel: 2 },
            SKAlphaType::Opaque,
            vec![0, 0],
        )
        .unwrap();
        assert_eq!(
            SkiaTextureCodec::to_managed_image(unsupported),
            Err(Error::InvalidOperation)
        );
    }

    #[test]
    fn bitmap_bounds_and_feature_disabled_decode_fail_typed() {
        assert_eq!(
            SKBitmap::new(0, 1, 0, SKColorType::Gray8, SKAlphaType::Opaque, Vec::new(),),
            Err(Error::Argument)
        );
        assert_eq!(
            SKBitmap::new(
                2,
                2,
                4,
                SKColorType::Rgba8888,
                SKAlphaType::Unpremul,
                vec![0; 16],
            ),
            Err(Error::Argument)
        );
        #[cfg(not(feature = "skia"))]
        assert_eq!(
            SkiaTextureCodec.decode(Box::new(std::io::Cursor::new(Vec::new()))),
            Err(Error::InvalidOperation)
        );
        #[cfg(not(feature = "rust-skia"))]
        assert_eq!(
            RustSkiaTextureCodec.decode(Box::new(std::io::Cursor::new(Vec::new()))),
            Err(Error::InvalidOperation)
        );
    }

    #[cfg(any(feature = "rust-skia", feature = "skia"))]
    fn hex_fixture(source: &str) -> Vec<u8> {
        let digits: Vec<_> = source.bytes().filter(u8::is_ascii_hexdigit).collect();
        assert_eq!(digits.len() % 2, 0, "fixture contains a partial byte");
        digits
            .chunks_exact(2)
            .map(|pair| {
                let high = (pair[0] as char).to_digit(16).expect("hex digit");
                let low = (pair[1] as char).to_digit(16).expect("hex digit");
                u8::try_from(high * 16 + low).expect("hex byte")
            })
            .collect()
    }

    #[cfg(feature = "rust-skia")]
    fn rust_decode(encoded: Vec<u8>) -> Result<ManagedImage, Error> {
        RustSkiaTextureCodec.decode(Box::new(std::io::Cursor::new(encoded)))
    }

    #[cfg(any(feature = "rust-skia", feature = "skia"))]
    fn checked_format_matrix() -> [(&'static str, &'static str, (i32, i32)); 19] {
        [
            (
                "BMP 16-bit RGB555",
                include_str!("../tests/fixtures/rgb555.bmp.hex"),
                (2, 1),
            ),
            (
                "BMP 24-bit bottom-up",
                include_str!("../tests/fixtures/bottom-up.bmp.hex"),
                (8, 8),
            ),
            (
                "BMP 32-bit BGRX",
                include_str!("../tests/fixtures/bgrx32.bmp.hex"),
                (2, 1),
            ),
            (
                "GIF palette and frame offset",
                include_str!("../tests/fixtures/palette-offset.gif.hex"),
                (4, 2),
            ),
            (
                "ICO largest entry",
                include_str!("../tests/fixtures/multi.ico.hex"),
                (2, 2),
            ),
            (
                "JPEG baseline RGB",
                include_str!("../tests/fixtures/baseline.jpg.hex"),
                (8, 8),
            ),
            (
                "JPEG progressive grayscale",
                include_str!("../tests/fixtures/progressive.jpg.hex"),
                (128, 128),
            ),
            (
                "PNG grayscale",
                include_str!("../tests/fixtures/gray.png.hex"),
                (32, 32),
            ),
            (
                "PNG grayscale alpha",
                include_str!("../tests/fixtures/gray-alpha.png.hex"),
                (32, 32),
            ),
            (
                "PNG RGB",
                include_str!("../tests/fixtures/rgb.png.hex"),
                (32, 32),
            ),
            (
                "PNG RGBA",
                include_str!("../tests/fixtures/rgba.png.hex"),
                (32, 32),
            ),
            (
                "PNG straight alpha",
                include_str!("../tests/fixtures/straight-alpha.png.hex"),
                (1, 1),
            ),
            (
                "PNG interlaced palette",
                include_str!("../tests/fixtures/interlaced-palette.png.hex"),
                (32, 32),
            ),
            (
                "WBMP multibyte width",
                include_str!("../tests/fixtures/multibyte.wbmp.hex"),
                (130, 1),
            ),
            (
                "WBMP minimum-length image",
                include_str!("../tests/fixtures/small.wbmp.hex"),
                (2, 2),
            ),
            (
                "WebP lossless RGB",
                include_str!("../tests/fixtures/rgb-lossless.webp.hex"),
                (2, 2),
            ),
            (
                "WebP lossless alpha",
                include_str!("../tests/fixtures/alpha.webp.hex"),
                (1, 1),
            ),
            (
                "WebP lossy",
                include_str!("../tests/fixtures/lossy-red.webp.hex"),
                (8, 8),
            ),
            (
                "WebP animated first frame",
                include_str!("../tests/fixtures/animated.webp.hex"),
                (11, 29),
            ),
        ]
    }

    #[cfg(any(feature = "rust-skia", feature = "skia"))]
    fn assert_checked_format_matrix(codec: &dyn libremetaverse_imaging::ITextureCodec) {
        for (name, fixture, dimensions) in checked_format_matrix() {
            let image = codec
                .decode(Box::new(std::io::Cursor::new(hex_fixture(fixture))))
                .unwrap_or_else(|error| panic!("{name} failed to decode: {error:?}"));
            assert_eq!((image.width, image.height), dimensions, "{name}");
            assert_eq!(
                image.channels,
                if matches!(
                    name,
                    "JPEG progressive grayscale"
                        | "PNG grayscale"
                        | "WBMP multibyte width"
                        | "WBMP minimum-length image"
                ) {
                    ManagedImageImageChannels::GRAY
                } else {
                    ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA
                },
                "{name}"
            );
        }
    }

    #[cfg(feature = "rust-skia")]
    #[test]
    fn rust_feature_decodes_the_checked_format_matrix() {
        assert_checked_format_matrix(&RustSkiaTextureCodec);
    }

    #[cfg(feature = "skia")]
    #[test]
    fn native_feature_decodes_the_checked_format_matrix() {
        assert_checked_format_matrix(&SkiaTextureCodec);
    }

    #[cfg(all(feature = "rust-skia", feature = "skia"))]
    #[test]
    fn native_and_rust_backends_match_the_shared_contract_matrix() {
        fn assert_plane(name: &str, plane: &str, native: &[u8], rust: &[u8], tolerance: u8) {
            assert_eq!(native.len(), rust.len(), "{name} {plane} length");
            for (index, (&native, &rust)) in native.iter().zip(rust).enumerate() {
                assert!(
                    native.abs_diff(rust) <= tolerance,
                    "{name} {plane}[{index}]: native={native}, rust={rust}, tolerance={tolerance}"
                );
            }
        }

        for (name, fixture, _) in checked_format_matrix() {
            let bytes = hex_fixture(fixture);
            let native = SkiaTextureCodec
                .decode(Box::new(std::io::Cursor::new(bytes.clone())))
                .unwrap_or_else(|error| panic!("native {name}: {error:?}"));
            let rust = RustSkiaTextureCodec
                .decode(Box::new(std::io::Cursor::new(bytes)))
                .unwrap_or_else(|error| panic!("Rust {name}: {error:?}"));
            assert_eq!(native.width, rust.width, "{name} width");
            assert_eq!(native.height, rust.height, "{name} height");
            assert_eq!(native.channels, rust.channels, "{name} channels");
            let tolerance = if name.starts_with("JPEG") {
                8
            } else if name == "WebP lossy" {
                10
            } else {
                0
            };
            assert_plane(name, "red", &native.red, &rust.red, tolerance);
            assert_plane(name, "green", &native.green, &rust.green, tolerance);
            assert_plane(name, "blue", &native.blue, &rust.blue, tolerance);
            assert_plane(name, "alpha", &native.alpha, &rust.alpha, tolerance);
        }
    }

    #[cfg(feature = "rust-skia")]
    #[test]
    fn rust_feature_preserves_canvas_orientation_selection_and_alpha() {
        for fixture in [
            include_str!("../tests/fixtures/two-by-two-bottom-up.bmp.hex"),
            include_str!("../tests/fixtures/two-by-two-top-down.bmp.hex"),
        ] {
            let bitmap = rust_decode(hex_fixture(fixture)).expect("oriented BMP");
            assert_eq!(bitmap.red, [255, 0, 0, 255]);
            assert_eq!(bitmap.green, [0, 255, 0, 255]);
            assert_eq!(bitmap.blue, [0, 0, 255, 255]);
        }

        let gif = rust_decode(hex_fixture(include_str!(
            "../tests/fixtures/palette-offset.gif.hex"
        )))
        .expect("GIF logical canvas");
        assert_eq!((gif.width, gif.height), (4, 2));

        let icon = rust_decode(hex_fixture(include_str!("../tests/fixtures/multi.ico.hex")))
            .expect("largest ICO entry");
        assert_eq!(icon.red, [255, 0, 0, 255]);
        assert_eq!(icon.green, [0, 255, 0, 255]);
        assert_eq!(icon.blue, [0, 0, 255, 255]);

        let alpha = rust_decode(hex_fixture(include_str!(
            "../tests/fixtures/alpha.webp.hex"
        )))
        .expect("alpha WebP");
        assert_eq!(
            (alpha.red[0], alpha.green[0], alpha.blue[0]),
            (255, 255, 255)
        );
        assert!((127..=128).contains(&alpha.alpha[0]));

        let lossy = rust_decode(hex_fixture(include_str!(
            "../tests/fixtures/lossy-red.webp.hex"
        )))
        .expect("lossy WebP");
        assert!(lossy.red.iter().all(|red| *red >= 245));
        assert!(lossy.green.iter().all(|green| *green <= 10));
        assert!(lossy.blue.iter().all(|blue| *blue <= 10));
        assert!(lossy.alpha.iter().all(|alpha| *alpha == u8::MAX));
    }

    #[cfg(feature = "rust-skia")]
    #[test]
    fn rust_feature_rejects_invalid_truncated_and_oversized_input_without_panicking() {
        for malformed in [Vec::new(), b"not an image".to_vec(), b"GIF89a".to_vec()] {
            let result =
                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| rust_decode(malformed)));
            assert!(result.is_ok(), "malformed input panicked");
            assert_eq!(result.expect("checked above"), Err(Error::InvalidOperation));
        }

        let mut pixel_bomb =
            hex_fixture(include_str!("../tests/fixtures/interlaced-palette.png.hex"));
        pixel_bomb[16..20].copy_from_slice(&4097_u32.to_be_bytes());
        pixel_bomb[20..24].copy_from_slice(&4097_u32.to_be_bytes());
        assert_eq!(rust_decode(pixel_bomb), Err(Error::Argument));

        let mut gif_frame_bomb =
            hex_fixture(include_str!("../tests/fixtures/palette-offset.gif.hex"));
        let descriptor = gif_frame_bomb[109..]
            .iter()
            .position(|byte| *byte == 0x2c)
            .map(|offset| offset + 109)
            .expect("first GIF image descriptor");
        gif_frame_bomb[descriptor + 5..descriptor + 7].copy_from_slice(&u16::MAX.to_le_bytes());
        gif_frame_bomb[descriptor + 7..descriptor + 9].copy_from_slice(&u16::MAX.to_le_bytes());
        assert_eq!(rust_decode(gif_frame_bomb), Err(Error::Argument));

        let mut malformed_gif_palette =
            hex_fixture(include_str!("../tests/fixtures/palette-offset.gif.hex"));
        // Advertise a 256-entry global palette while retaining the fixture's
        // much smaller checked table. Preflight must reject the missing table
        // before the decoder can allocate or index palette storage.
        malformed_gif_palette[10] = (malformed_gif_palette[10] & 0xf8) | 0x07;
        assert_eq!(
            rust_decode(malformed_gif_palette),
            Err(Error::InvalidOperation)
        );

        let mut icon_embedded_bomb = hex_fixture(include_str!("../tests/fixtures/multi.ico.hex"));
        icon_embedded_bomb[86..90].copy_from_slice(&4097_i32.to_le_bytes());
        icon_embedded_bomb[90..94].copy_from_slice(&8194_i32.to_le_bytes());
        assert_eq!(rust_decode(icon_embedded_bomb), Err(Error::Argument));

        let over_limit = vec![0_u8; DEFAULT_MAX_ENCODED_BYTES + 1];
        assert_eq!(rust_decode(over_limit), Err(Error::Argument));

        for fixture in [
            include_str!("../tests/fixtures/bottom-up.bmp.hex"),
            include_str!("../tests/fixtures/palette-offset.gif.hex"),
            include_str!("../tests/fixtures/multi.ico.hex"),
            include_str!("../tests/fixtures/interlaced-palette.png.hex"),
            include_str!("../tests/fixtures/alpha.webp.hex"),
        ] {
            let mut bytes = hex_fixture(fixture);
            bytes.truncate(bytes.len() / 2);
            let result =
                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| rust_decode(bytes)));
            assert!(result.is_ok(), "truncated input panicked");
            assert!(result.expect("checked above").is_err());
        }
    }

    #[cfg(feature = "rust-skia")]
    struct DropStream {
        cursor: std::io::Cursor<Vec<u8>>,
        drops: std::sync::Arc<std::sync::atomic::AtomicUsize>,
    }

    #[cfg(feature = "rust-skia")]
    impl std::io::Read for DropStream {
        fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
            std::io::Read::read(&mut self.cursor, buffer)
        }
    }

    #[cfg(feature = "rust-skia")]
    impl std::io::Write for DropStream {
        fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
            std::io::Write::write(&mut self.cursor, buffer)
        }

        fn flush(&mut self) -> std::io::Result<()> {
            Ok(())
        }
    }

    #[cfg(feature = "rust-skia")]
    impl std::io::Seek for DropStream {
        fn seek(&mut self, position: std::io::SeekFrom) -> std::io::Result<u64> {
            std::io::Seek::seek(&mut self.cursor, position)
        }
    }

    #[cfg(feature = "rust-skia")]
    impl Drop for DropStream {
        fn drop(&mut self) {
            self.drops.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
        }
    }

    #[cfg(feature = "rust-skia")]
    #[test]
    fn rust_feature_is_thread_safe_across_repeated_decode_and_drop() {
        use std::sync::Arc;
        use std::sync::atomic::{AtomicUsize, Ordering};

        fn assert_texture_codec<T: libremetaverse_imaging::ITextureCodec + Send + Sync>() {}
        assert_texture_codec::<RustSkiaTextureCodec>();

        let codec = Arc::new(RustSkiaTextureCodec);
        let png = Arc::new(hex_fixture(include_str!(
            "../tests/fixtures/interlaced-palette.png.hex"
        )));
        let mut workers = Vec::new();
        for _ in 0..8 {
            let codec = Arc::clone(&codec);
            let png = Arc::clone(&png);
            workers.push(std::thread::spawn(move || {
                for _ in 0..16 {
                    let image = codec
                        .decode(Box::new(std::io::Cursor::new((*png).clone())))
                        .expect("concurrent PNG decode");
                    assert_eq!((image.width, image.height), (32, 32));
                    let repeated = codec
                        .decode(Box::new(std::io::Cursor::new((*png).clone())))
                        .expect("repeated concurrent PNG decode");
                    assert_eq!(image, repeated);
                }
            }));
        }
        for worker in workers {
            worker.join().expect("decoder worker did not panic");
        }

        let drops = Arc::new(AtomicUsize::new(0));
        let stream = DropStream {
            cursor: std::io::Cursor::new((*png).clone()),
            drops: Arc::clone(&drops),
        };
        codec.decode(Box::new(stream)).expect("drop-probe decode");
        assert_eq!(drops.load(Ordering::SeqCst), 1);
    }

    #[cfg(feature = "skia")]
    #[test]
    fn feature_decodes_known_png_with_straight_alpha() {
        const PNG: &[u8] = &[
            0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48,
            0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00,
            0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x44, 0x41, 0x54, 0x78,
            0x9c, 0x63, 0xf8, 0xcf, 0xc0, 0xf0, 0x1f, 0x00, 0x05, 0x00, 0x01, 0xff, 0x89, 0x99,
            0x3d, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
        ];
        let image = SkiaTextureCodec
            .decode(Box::new(std::io::Cursor::new(PNG.to_vec())))
            .expect("decode PNG");
        assert_eq!((image.width, image.height), (1, 1));
        assert_eq!(image.red, [255]);
        assert_eq!(image.green, [0]);
        assert_eq!(image.blue, [0]);
        assert_eq!(image.alpha, [255]);

        assert_eq!(
            SkiaTextureCodec.decode(Box::new(std::io::Cursor::new(Vec::new()))),
            Err(Error::InvalidOperation)
        );
        assert_eq!(
            SkiaTextureCodec.decode(Box::new(std::io::Cursor::new(b"not an image".to_vec()))),
            Err(Error::InvalidOperation)
        );
    }

    #[cfg(feature = "skia")]
    #[test]
    fn feature_decodes_webp_from_the_cross_platform_cache_configuration() {
        const WEBP: &[u8] = &[
            0x52, 0x49, 0x46, 0x46, 0x1c, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50,
            0x38, 0x4c, 0x0f, 0x00, 0x00, 0x00, 0x2f, 0x01, 0x40, 0x00, 0x00, 0x07, 0x10, 0xf5,
            0x8f, 0xfe, 0x07, 0x22, 0xa2, 0xff, 0x01, 0x00,
        ];
        let image = SkiaTextureCodec
            .decode(Box::new(std::io::Cursor::new(WEBP.to_vec())))
            .expect("decode WebP");
        assert_eq!((image.width, image.height), (2, 2));
        assert_eq!(image.red, [254; 4]);
        assert_eq!(image.green, [0; 4]);
        assert_eq!(image.blue, [0; 4]);
        assert_eq!(image.alpha, [255; 4]);
    }
}
