//! Owned planar image storage and the codec abstraction boundary.

use crate::Error;
use libremetaverse_types::compat::ReadWrite;
use std::ops::{BitAnd, BitOr, BitXor, Not};

/// Maximum number of decoded pixels accepted by [`ManagedImage`].
///
/// This permits a 4096 by 4096 texture while rejecting hostile dimensions
/// before any channel allocation occurs.
pub const DEFAULT_MAX_PIXELS: usize = 4096 * 4096;

/// Maximum encoded input size codec adapters may buffer by default.
pub const DEFAULT_MAX_ENCODED_BYTES: usize = 64 * 1024 * 1024;

const MAX_CHANNEL_BYTES: usize = DEFAULT_MAX_PIXELS * 5;

/// Object-safe boundary for compressed-image decoders.
///
/// Implementations must bound buffered input to [`DEFAULT_MAX_ENCODED_BYTES`],
/// decode through the checked [`ManagedImage`] constructors, and return a typed
/// error for malformed data. Native codec-specific types do not cross this
/// boundary.
pub trait ITextureCodec {
    /// Decodes a stream positioned at the beginning of an encoded image.
    ///
    /// # Errors
    ///
    /// Returns a typed parse, argument, allocation, or I/O-class error when
    /// the encoded stream cannot be decoded within the documented limits.
    fn decode(&self, stream: Box<dyn ReadWrite + Send>) -> Result<ManagedImage, Error>;
}

/// Flags selecting the planar channels stored by [`ManagedImage`].
///
/// Unknown bits are retained for C# flag-enum compatibility. Gray and color
/// are both legal bits; when both are present, the reference constructor gives
/// gray storage precedence.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
#[repr(transparent)]
pub struct ManagedImageImageChannels(pub i32);

impl ManagedImageImageChannels {
    /// One gray plane stored in [`ManagedImage::red`].
    pub const GRAY: Self = Self(1);
    /// Three color planes stored in red, green, blue order.
    pub const COLOR: Self = Self(2);
    /// One alpha plane.
    pub const ALPHA: Self = Self(4);
    /// One bump plane.
    pub const BUMP: Self = Self(8);

    /// Returns whether all bits in `other` are present.
    #[must_use]
    pub const fn contains(self, other: Self) -> bool {
        self.0 & other.0 == other.0
    }
}

impl BitAnd for ManagedImageImageChannels {
    type Output = Self;

    fn bitand(self, rhs: Self) -> Self::Output {
        Self(self.0 & rhs.0)
    }
}

impl BitOr for ManagedImageImageChannels {
    type Output = Self;

    fn bitor(self, rhs: Self) -> Self::Output {
        Self(self.0 | rhs.0)
    }
}

impl BitXor for ManagedImageImageChannels {
    type Output = Self;

    fn bitxor(self, rhs: Self) -> Self::Output {
        Self(self.0 ^ rhs.0)
    }
}

impl Not for ManagedImageImageChannels {
    type Output = Self;

    fn not(self) -> Self::Output {
        Self(!self.0)
    }
}

/// An owned image with one byte per sample and one allocation per channel.
///
/// Rows use a top-left origin. Every present plane has exactly `width * height`
/// bytes and a row stride of `width`; gray samples occupy [`Self::red`]. The
/// fields remain public to match the C# surface, so methods validate the layout
/// before indexing it and return [`Error::InvalidOperation`] if a caller has
/// supplied inconsistent buffers.
#[derive(Debug, Eq, PartialEq)]
pub struct ManagedImage {
    /// Alpha samples, or an empty vector when alpha is absent.
    pub alpha: Vec<u8>,
    /// Blue samples, or an empty vector when color is absent.
    pub blue: Vec<u8>,
    /// Bump samples, or an empty vector when bump is absent.
    pub bump: Vec<u8>,
    /// Channel flags describing the planar buffers.
    pub channels: ManagedImageImageChannels,
    /// Green samples, or an empty vector when color is absent.
    pub green: Vec<u8>,
    /// Image height in pixels.
    pub height: i32,
    /// Red or gray samples, or an empty vector when neither is present.
    pub red: Vec<u8>,
    /// Image width in pixels.
    pub width: i32,
}

impl ManagedImage {
    /// Creates a blank image using the reference planar channel layout.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Argument`] for non-positive, overflowing, or over-limit
    /// dimensions, and [`Error::InvalidOperation`] if allocation fails.
    pub fn new(
        width: i32,
        height: i32,
        channels: ManagedImageImageChannels,
    ) -> Result<Self, Error> {
        let pixels = checked_pixels(width, height)?;
        let gray = channels.contains(ManagedImageImageChannels::GRAY);
        let color = channels.contains(ManagedImageImageChannels::COLOR);
        let alpha = channels.contains(ManagedImageImageChannels::ALPHA);
        let bump = channels.contains(ManagedImageImageChannels::BUMP);
        checked_storage(
            pixels,
            usize::from(gray || color)
                + 2 * usize::from(color && !gray)
                + usize::from(alpha)
                + usize::from(bump),
        )?;

        let red = allocate_plane(if gray || color { pixels } else { 0 }, 0)?;
        let (green, blue) = if color && !gray {
            (allocate_plane(pixels, 0)?, allocate_plane(pixels, 0)?)
        } else {
            (Vec::new(), Vec::new())
        };
        Ok(Self {
            alpha: allocate_plane(if alpha { pixels } else { 0 }, 0)?,
            blue,
            bump: allocate_plane(if bump { pixels } else { 0 }, 0)?,
            channels,
            green,
            height,
            red,
            width,
        })
    }

    /// Validates dimensions, limits, and all public channel-buffer lengths.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Argument`] for invalid dimensions and
    /// [`Error::InvalidOperation`] for inconsistent public channel buffers.
    pub fn validate(&self) -> Result<(), Error> {
        let pixels = checked_pixels(self.width, self.height)?;
        let gray = self.channels.contains(ManagedImageImageChannels::GRAY);
        let color = self.channels.contains(ManagedImageImageChannels::COLOR);
        let expected_red = if gray || color { pixels } else { 0 };
        let expected_color = if color && !gray { pixels } else { 0 };
        let expected_alpha = if self.channels.contains(ManagedImageImageChannels::ALPHA) {
            pixels
        } else {
            0
        };
        let expected_bump = if self.channels.contains(ManagedImageImageChannels::BUMP) {
            pixels
        } else {
            0
        };
        checked_storage(
            pixels,
            usize::from(expected_red != 0)
                + 2 * usize::from(expected_color != 0)
                + usize::from(expected_alpha != 0)
                + usize::from(expected_bump != 0),
        )?;
        if self.red.len() != expected_red
            || self.green.len() != expected_color
            || self.blue.len() != expected_color
            || self.alpha.len() != expected_alpha
            || self.bump.len() != expected_bump
        {
            return Err(Error::InvalidOperation);
        }
        Ok(())
    }

    /// Sets every allocated channel sample to zero.
    ///
    /// # Errors
    ///
    /// This fixed compatibility signature currently cannot fail.
    pub fn clear(&mut self) -> Result<(), Error> {
        self.red.fill(0);
        self.green.fill(0);
        self.blue.fill(0);
        self.alpha.fill(0);
        self.bump.fill(0);
        Ok(())
    }

    /// Creates an independent deep copy of the image and its channel buffers.
    ///
    /// # Errors
    ///
    /// Returns an error for an invalid source layout or failed bounded
    /// allocation.
    #[allow(clippy::should_implement_trait)] // The mapped C# Clone returns Result<Self, Error>.
    pub fn clone(&self) -> Result<Self, Error> {
        self.validate()?;
        Ok(Self {
            alpha: copy_plane(&self.alpha)?,
            blue: copy_plane(&self.blue)?,
            bump: copy_plane(&self.bump)?,
            channels: self.channels,
            green: copy_plane(&self.green)?,
            height: self.height,
            red: copy_plane(&self.red)?,
            width: self.width,
        })
    }

    /// Converts channel storage using the same add/remove rules as the C# type.
    ///
    /// # Errors
    ///
    /// Returns an error for invalid dimensions or failed bounded allocation.
    pub fn convert_channels(&mut self, channels: ManagedImageImageChannels) -> Result<(), Error> {
        if self.channels == channels {
            return Ok(());
        }
        let pixels = checked_pixels(self.width, self.height)?;
        let add = (self.channels ^ channels) & channels;
        let delete = (self.channels ^ channels) & self.channels;

        let added_color = if add.contains(ManagedImageImageChannels::COLOR) {
            Some((
                allocate_plane(pixels, 0)?,
                allocate_plane(pixels, 0)?,
                allocate_plane(pixels, 0)?,
            ))
        } else {
            None
        };
        let added_alpha = if add.contains(ManagedImageImageChannels::ALPHA) {
            Some(allocate_plane(pixels, u8::MAX)?)
        } else {
            None
        };
        let added_bump = if add.contains(ManagedImageImageChannels::BUMP) {
            Some(allocate_plane(pixels, 0)?)
        } else {
            None
        };

        if let Some((red, green, blue)) = added_color {
            self.red = red;
            self.green = green;
            self.blue = blue;
        } else if delete.contains(ManagedImageImageChannels::COLOR) {
            self.red.clear();
            self.green.clear();
            self.blue.clear();
        }
        if let Some(alpha) = added_alpha {
            self.alpha = alpha;
        } else if delete.contains(ManagedImageImageChannels::ALPHA) {
            self.alpha.clear();
        }
        if let Some(bump) = added_bump {
            self.bump = bump;
        } else if delete.contains(ManagedImageImageChannels::BUMP) {
            self.bump.clear();
        }
        self.channels = channels;
        Ok(())
    }

    /// Exports bottom-left-origin, interleaved 32-bit RGBA data as in C#.
    ///
    /// # Errors
    ///
    /// Returns an error when dimensions or public planes are inconsistent,
    /// the reference RGBA conversion is undefined, or allocation fails.
    pub fn export_raw(&self) -> Result<Vec<u8>, Error> {
        self.validate()?;
        let pixels = checked_pixels(self.width, self.height)?;
        let length = pixels.checked_mul(4).ok_or(Error::Argument)?;
        if length > MAX_CHANNEL_BYTES {
            return Err(Error::Argument);
        }
        let alpha = self.channels.contains(ManagedImageImageChannels::ALPHA);
        let color = self.channels.contains(ManagedImageImageChannels::COLOR);
        if !alpha && !color {
            return Err(Error::InvalidOperation);
        }
        if color && (self.green.len() != pixels || self.blue.len() != pixels) {
            return Err(Error::InvalidOperation);
        }
        let mut raw = allocate_plane(length, 0)?;
        let width = usize::try_from(self.width).map_err(|_| Error::Argument)?;
        let height = usize::try_from(self.height).map_err(|_| Error::Argument)?;
        for y in 0..height {
            for x in 0..width {
                let source = y * width + x;
                let target = ((height - 1 - y) * width + x) * 4;
                if alpha && !color {
                    raw[target..target + 3].fill(self.alpha[source]);
                    raw[target + 3] = u8::MAX;
                } else {
                    raw[target] = self.red[source];
                    raw[target + 1] = self.green[source];
                    raw[target + 2] = self.blue[source];
                    raw[target + 3] = if alpha { self.alpha[source] } else { u8::MAX };
                }
            }
        }
        Ok(raw)
    }

    /// Resizes every present plane using nearest-neighbor sampling.
    ///
    /// # Errors
    ///
    /// Returns an error for invalid target dimensions, inconsistent source
    /// planes, checked arithmetic failure, or failed bounded allocation.
    pub fn resize_nearest_neighbor(&mut self, width: i32, height: i32) -> Result<(), Error> {
        checked_pixels(width, height)?;
        self.validate()?;
        if width == self.width && height == self.height {
            return Ok(());
        }
        let old_width = usize::try_from(self.width).map_err(|_| Error::Argument)?;
        let old_height = usize::try_from(self.height).map_err(|_| Error::Argument)?;
        let new_width = usize::try_from(width).map_err(|_| Error::Argument)?;
        let new_height = usize::try_from(height).map_err(|_| Error::Argument)?;

        let red = resize_nearest_plane(&self.red, old_width, old_height, new_width, new_height)?;
        let green =
            resize_nearest_plane(&self.green, old_width, old_height, new_width, new_height)?;
        let blue = resize_nearest_plane(&self.blue, old_width, old_height, new_width, new_height)?;
        let alpha =
            resize_nearest_plane(&self.alpha, old_width, old_height, new_width, new_height)?;
        let bump = resize_nearest_plane(&self.bump, old_width, old_height, new_width, new_height)?;
        self.width = width;
        self.height = height;
        self.red = red;
        self.green = green;
        self.blue = blue;
        self.alpha = alpha;
        self.bump = bump;
        Ok(())
    }

    /// Resizes every present plane using C#-compatible bilinear interpolation.
    ///
    /// # Errors
    ///
    /// Returns an error for invalid target dimensions, inconsistent source
    /// planes, checked arithmetic failure, or failed bounded allocation.
    pub fn resize_bilinear(&mut self, width: i32, height: i32) -> Result<(), Error> {
        checked_pixels(width, height)?;
        self.validate()?;
        if width == self.width && height == self.height {
            return Ok(());
        }
        if self.width <= 1 || self.height <= 1 {
            return self.resize_nearest_neighbor(width, height);
        }
        let old_width = usize::try_from(self.width).map_err(|_| Error::Argument)?;
        let old_height = usize::try_from(self.height).map_err(|_| Error::Argument)?;
        let new_width = usize::try_from(width).map_err(|_| Error::Argument)?;
        let new_height = usize::try_from(height).map_err(|_| Error::Argument)?;

        let red = resize_bilinear_plane(&self.red, old_width, old_height, new_width, new_height)?;
        let green =
            resize_bilinear_plane(&self.green, old_width, old_height, new_width, new_height)?;
        let blue = resize_bilinear_plane(&self.blue, old_width, old_height, new_width, new_height)?;
        let alpha =
            resize_bilinear_plane(&self.alpha, old_width, old_height, new_width, new_height)?;
        let bump = resize_bilinear_plane(&self.bump, old_width, old_height, new_width, new_height)?;
        self.width = width;
        self.height = height;
        self.red = red;
        self.green = green;
        self.blue = blue;
        self.alpha = alpha;
        self.bump = bump;
        Ok(())
    }

    /// Builds planar storage from canonical top-left-origin interleaved bytes.
    ///
    /// Component order is gray or RGB, followed by alpha and then bump when
    /// those flags are present. `stride` is the byte distance between rows and
    /// may include trailing padding.
    ///
    /// # Errors
    ///
    /// Returns an error for invalid dimensions, ambiguous channel flags,
    /// insufficient input/stride, checked arithmetic failure, or allocation
    /// beyond the image limits.
    pub fn from_interleaved(
        width: i32,
        height: i32,
        channels: ManagedImageImageChannels,
        stride: usize,
        bytes: &[u8],
    ) -> Result<Self, Error> {
        let pixels = checked_pixels(width, height)?;
        let components = canonical_components(channels)?;
        let width_usize = usize::try_from(width).map_err(|_| Error::Argument)?;
        let height_usize = usize::try_from(height).map_err(|_| Error::Argument)?;
        let row_bytes = width_usize.checked_mul(components).ok_or(Error::Argument)?;
        let required = required_interleaved_bytes(height_usize, stride, row_bytes)?;
        if stride < row_bytes || required > bytes.len() || required > MAX_CHANNEL_BYTES {
            return Err(Error::Argument);
        }
        let mut image = Self::new(width, height, channels)?;
        for y in 0..height_usize {
            for x in 0..width_usize {
                let pixel = y * width_usize + x;
                let mut source = y * stride + x * components;
                if channels.contains(ManagedImageImageChannels::GRAY) {
                    image.red[pixel] = bytes[source];
                    source += 1;
                } else if channels.contains(ManagedImageImageChannels::COLOR) {
                    image.red[pixel] = bytes[source];
                    image.green[pixel] = bytes[source + 1];
                    image.blue[pixel] = bytes[source + 2];
                    source += 3;
                }
                if channels.contains(ManagedImageImageChannels::ALPHA) {
                    image.alpha[pixel] = bytes[source];
                    source += 1;
                }
                if channels.contains(ManagedImageImageChannels::BUMP) {
                    image.bump[pixel] = bytes[source];
                }
            }
        }
        debug_assert_eq!(
            pixels,
            image.red.len().max(image.alpha.len()).max(image.bump.len())
        );
        Ok(image)
    }

    /// Exports canonical top-left-origin interleaved bytes with a caller-chosen stride.
    ///
    /// # Errors
    ///
    /// Returns an error for an inconsistent image, ambiguous channel flags,
    /// insufficient stride, checked arithmetic failure, or failed allocation.
    pub fn to_interleaved(&self, stride: usize) -> Result<Vec<u8>, Error> {
        self.validate()?;
        let components = canonical_components(self.channels)?;
        let width = usize::try_from(self.width).map_err(|_| Error::Argument)?;
        let height = usize::try_from(self.height).map_err(|_| Error::Argument)?;
        let row_bytes = width.checked_mul(components).ok_or(Error::Argument)?;
        let minimum = required_interleaved_bytes(height, stride, row_bytes)?;
        let length = stride.checked_mul(height).ok_or(Error::Argument)?;
        if stride < row_bytes || minimum > length || length > MAX_CHANNEL_BYTES {
            return Err(Error::Argument);
        }
        let mut bytes = allocate_plane(length, 0)?;
        for y in 0..height {
            for x in 0..width {
                let pixel = y * width + x;
                let mut target = y * stride + x * components;
                if self.channels.contains(ManagedImageImageChannels::GRAY) {
                    bytes[target] = self.red[pixel];
                    target += 1;
                } else if self.channels.contains(ManagedImageImageChannels::COLOR) {
                    bytes[target] = self.red[pixel];
                    bytes[target + 1] = self.green[pixel];
                    bytes[target + 2] = self.blue[pixel];
                    target += 3;
                }
                if self.channels.contains(ManagedImageImageChannels::ALPHA) {
                    bytes[target] = self.alpha[pixel];
                    target += 1;
                }
                if self.channels.contains(ManagedImageImageChannels::BUMP) {
                    bytes[target] = self.bump[pixel];
                }
            }
        }
        Ok(bytes)
    }
}

fn checked_pixels(width: i32, height: i32) -> Result<usize, Error> {
    let width = usize::try_from(width).map_err(|_| Error::Argument)?;
    let height = usize::try_from(height).map_err(|_| Error::Argument)?;
    if width == 0 || height == 0 {
        return Err(Error::Argument);
    }
    let pixels = width.checked_mul(height).ok_or(Error::Argument)?;
    if pixels > DEFAULT_MAX_PIXELS {
        return Err(Error::Argument);
    }
    Ok(pixels)
}

fn checked_storage(pixels: usize, planes: usize) -> Result<(), Error> {
    if pixels.checked_mul(planes).ok_or(Error::Argument)? > MAX_CHANNEL_BYTES {
        Err(Error::Argument)
    } else {
        Ok(())
    }
}

fn allocate_plane(length: usize, value: u8) -> Result<Vec<u8>, Error> {
    if length > MAX_CHANNEL_BYTES {
        return Err(Error::Argument);
    }
    let mut plane = Vec::new();
    plane
        .try_reserve_exact(length)
        .map_err(|_| Error::InvalidOperation)?;
    plane.resize(length, value);
    Ok(plane)
}

fn copy_plane(source: &[u8]) -> Result<Vec<u8>, Error> {
    let mut copy = allocate_plane(source.len(), 0)?;
    copy.copy_from_slice(source);
    Ok(copy)
}

fn resize_nearest_plane(
    source: &[u8],
    old_width: usize,
    old_height: usize,
    new_width: usize,
    new_height: usize,
) -> Result<Vec<u8>, Error> {
    if source.is_empty() {
        return Ok(Vec::new());
    }
    let mut target = allocate_plane(new_width.checked_mul(new_height).ok_or(Error::Argument)?, 0)?;
    for y in 0..new_height {
        let source_y = y.checked_mul(old_height).ok_or(Error::Argument)? / new_height;
        for x in 0..new_width {
            let source_x = x.checked_mul(old_width).ok_or(Error::Argument)? / new_width;
            target[y * new_width + x] = source[source_y * old_width + source_x];
        }
    }
    Ok(target)
}

#[allow(clippy::cast_precision_loss)] // C# explicitly performs these coordinates in f32.
fn resize_bilinear_plane(
    source: &[u8],
    old_width: usize,
    old_height: usize,
    new_width: usize,
    new_height: usize,
) -> Result<Vec<u8>, Error> {
    if source.is_empty() {
        return Ok(Vec::new());
    }
    let mut target = allocate_plane(new_width.checked_mul(new_height).ok_or(Error::Argument)?, 0)?;
    let x_scale = (old_width - 1) as f32 / (new_width.saturating_sub(1).max(1)) as f32;
    let y_scale = (old_height - 1) as f32 / (new_height.saturating_sub(1).max(1)) as f32;
    for y in 0..new_height {
        let source_y = if new_height > 1 {
            y as f32 * y_scale
        } else {
            0.0
        };
        for x in 0..new_width {
            let source_x = if new_width > 1 {
                x as f32 * x_scale
            } else {
                0.0
            };
            target[y * new_width + x] =
                bilinear_sample(source, old_width, old_height, source_x, source_y);
        }
    }
    Ok(target)
}

#[allow(
    clippy::cast_possible_truncation,
    clippy::cast_precision_loss,
    clippy::cast_sign_loss
)] // Coordinates are finite, non-negative, bounded image indices; output is clamped.
fn bilinear_sample(source: &[u8], width: usize, height: usize, x: f32, y: f32) -> u8 {
    let x0 = x as usize;
    let y0 = y as usize;
    let x1 = (x0 + 1).min(width - 1);
    let y1 = (y0 + 1).min(height - 1);
    let fraction_x = x - x0 as f32;
    let fraction_y = y - y0 as f32;
    let top = f32::from(source[y0 * width + x0]) * (1.0 - fraction_x)
        + f32::from(source[y0 * width + x1]) * fraction_x;
    let bottom = f32::from(source[y1 * width + x0]) * (1.0 - fraction_x)
        + f32::from(source[y1 * width + x1]) * fraction_x;
    (top * (1.0 - fraction_y) + bottom * fraction_y)
        .clamp(0.0, 255.0)
        .round_ties_even() as u8
}

fn canonical_components(channels: ManagedImageImageChannels) -> Result<usize, Error> {
    let gray = channels.contains(ManagedImageImageChannels::GRAY);
    let color = channels.contains(ManagedImageImageChannels::COLOR);
    if gray && color {
        return Err(Error::Argument);
    }
    let count = usize::from(gray)
        + 3 * usize::from(color)
        + usize::from(channels.contains(ManagedImageImageChannels::ALPHA))
        + usize::from(channels.contains(ManagedImageImageChannels::BUMP));
    if count == 0 {
        Err(Error::Argument)
    } else {
        Ok(count)
    }
}

fn required_interleaved_bytes(
    height: usize,
    stride: usize,
    row_bytes: usize,
) -> Result<usize, Error> {
    height
        .checked_sub(1)
        .and_then(|rows| rows.checked_mul(stride))
        .and_then(|prefix| prefix.checked_add(row_bytes))
        .ok_or(Error::Argument)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::{Cursor, Read as _};

    fn rgba() -> ManagedImageImageChannels {
        ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA
    }

    #[test]
    fn construction_layout_and_limits_are_checked_before_allocation() {
        let gray_alpha = ManagedImage::new(
            3,
            2,
            ManagedImageImageChannels::GRAY | ManagedImageImageChannels::ALPHA,
        )
        .unwrap();
        assert_eq!(gray_alpha.red.len(), 6);
        assert_eq!(gray_alpha.alpha.len(), 6);
        assert!(gray_alpha.green.is_empty());
        assert_eq!(ManagedImage::new(0, 1, rgba()), Err(Error::Argument));
        assert_eq!(ManagedImage::new(-1, 1, rgba()), Err(Error::Argument));
        assert_eq!(
            ManagedImage::new(i32::MAX, i32::MAX, rgba()),
            Err(Error::Argument)
        );
        let unknown = ManagedImageImageChannels(16) | ManagedImageImageChannels::ALPHA;
        assert_eq!(ManagedImage::new(1, 1, unknown).unwrap().channels, unknown);
    }

    #[test]
    fn channel_conversion_clear_and_clone_match_reference_ownership() {
        let mut image = ManagedImage::new(2, 2, ManagedImageImageChannels::COLOR).unwrap();
        image.red.copy_from_slice(&[1, 2, 3, 4]);
        image
            .convert_channels(rgba() | ManagedImageImageChannels::BUMP)
            .unwrap();
        assert_eq!(image.alpha, vec![255; 4]);
        assert_eq!(image.bump, vec![0; 4]);
        let mut copy = image.clone().unwrap();
        copy.red[0] = 99;
        assert_eq!(image.red[0], 1);
        copy.clear().unwrap();
        assert!(copy.red.iter().all(|value| *value == 0));
    }

    #[test]
    fn canonical_interleave_round_trip_honors_padded_stride() {
        let bytes = [
            1, 2, 3, 4, 5, 6, 7, 8, 99, 99, 9, 10, 11, 12, 13, 14, 15, 16, 99, 99,
        ];
        let image = ManagedImage::from_interleaved(2, 2, rgba(), 10, &bytes).unwrap();
        assert_eq!(image.red, vec![1, 5, 9, 13]);
        assert_eq!(image.green, vec![2, 6, 10, 14]);
        assert_eq!(image.blue, vec![3, 7, 11, 15]);
        assert_eq!(image.alpha, vec![4, 8, 12, 16]);
        assert_eq!(
            image.to_interleaved(10).unwrap(),
            [
                1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 9, 10, 11, 12, 13, 14, 15, 16, 0, 0,
            ]
        );
        assert_eq!(
            ManagedImage::from_interleaved(2, 2, rgba(), 7, &bytes),
            Err(Error::Argument)
        );
    }

    #[test]
    fn nearest_and_bilinear_resize_all_present_planes() {
        let mut nearest =
            ManagedImage::new(2, 2, rgba() | ManagedImageImageChannels::BUMP).unwrap();
        nearest.red.copy_from_slice(&[1, 2, 3, 4]);
        nearest.green.copy_from_slice(&[10, 20, 30, 40]);
        nearest.blue.copy_from_slice(&[100, 110, 120, 130]);
        nearest.alpha.copy_from_slice(&[200, 201, 202, 203]);
        nearest.bump.copy_from_slice(&[50, 51, 52, 53]);
        nearest.resize_nearest_neighbor(4, 4).unwrap();
        assert_eq!(
            nearest.red,
            vec![1, 1, 2, 2, 1, 1, 2, 2, 3, 3, 4, 4, 3, 3, 4, 4]
        );
        assert_eq!(nearest.green[15], 40);
        assert_eq!(nearest.blue[15], 130);
        assert_eq!(nearest.alpha[15], 203);
        assert_eq!(nearest.bump[15], 53);

        let mut bilinear = ManagedImage::new(2, 2, ManagedImageImageChannels::GRAY).unwrap();
        bilinear.red.copy_from_slice(&[0, 10, 20, 30]);
        bilinear.resize_bilinear(3, 3).unwrap();
        assert_eq!(bilinear.red, vec![0, 5, 10, 10, 15, 20, 20, 25, 30]);
    }

    #[test]
    fn export_raw_preserves_reference_flip_and_alpha_only_behavior() {
        let mut image = ManagedImage::new(1, 2, rgba()).unwrap();
        image.red.copy_from_slice(&[1, 2]);
        image.green.copy_from_slice(&[3, 4]);
        image.blue.copy_from_slice(&[5, 6]);
        image.alpha.copy_from_slice(&[7, 8]);
        assert_eq!(image.export_raw().unwrap(), [2, 4, 6, 8, 1, 3, 5, 7]);

        let mut alpha = ManagedImage::new(1, 1, ManagedImageImageChannels::ALPHA).unwrap();
        alpha.alpha[0] = 42;
        assert_eq!(alpha.export_raw().unwrap(), [42, 42, 42, 255]);
    }

    #[test]
    fn malformed_public_layouts_return_typed_errors_without_partial_resize() {
        let mut image = ManagedImage::new(2, 2, ManagedImageImageChannels::COLOR).unwrap();
        image.green.pop();
        assert_eq!(image.validate(), Err(Error::InvalidOperation));
        assert_eq!(image.export_raw(), Err(Error::InvalidOperation));
        assert_eq!(
            image.resize_nearest_neighbor(4, 4),
            Err(Error::InvalidOperation)
        );
        assert_eq!((image.width, image.height), (2, 2));
    }

    struct OnePixelCodec;

    impl ITextureCodec for OnePixelCodec {
        fn decode(&self, mut stream: Box<dyn ReadWrite + Send>) -> Result<ManagedImage, Error> {
            let mut sample = [0];
            stream.read_exact(&mut sample).map_err(|_| Error::Parse {
                position: 0,
                context: "missing gray sample",
            })?;
            let mut image = ManagedImage::new(1, 1, ManagedImageImageChannels::GRAY)?;
            image.red[0] = sample[0];
            Ok(image)
        }
    }

    #[test]
    fn texture_codec_is_object_safe_and_uses_only_core_abstractions() {
        let codec: &dyn ITextureCodec = &OnePixelCodec;
        let image = codec
            .decode(Box::new(Cursor::new(vec![73])))
            .expect("one-pixel decode");
        assert_eq!(image.red, [73]);
    }
}
