//! Backend-neutral JPEG 2000 contracts with independently selectable codecs.

use crate::codec::{InterleavedComponent, InterleavedImage};
use crate::{
    DEFAULT_MAX_ENCODED_BYTES, DEFAULT_MAX_PIXELS, Error, ITextureCodec, ManagedImage,
    ManagedImageImageChannels,
};
#[cfg(feature = "rust-j2k")]
use j2k as rust_j2k;
#[cfg(feature = "jpeg2000")]
use libremetaverse_openjpeg as openjpeg;
use libremetaverse_types::compat::ReadWrite;
#[cfg(feature = "rust-j2k")]
use std::borrow::Cow;
use std::io::Read;

/// JPEG 2000 container selection.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum J2kFormat {
    /// Raw JPEG 2000 codestream used by Second Life texture assets.
    #[default]
    Codestream,
    /// JP2 file-format container.
    Jp2,
}

/// JPEG 2000 wavelet and rate-control mode.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum J2kCompression {
    /// Reversible 5/3 wavelet with exact sample reconstruction.
    #[default]
    Lossless,
    /// Irreversible 9/7 wavelet targeting the given compression ratio.
    Lossy {
        /// Uncompressed bytes divided by target codestream bytes. Must be at
        /// least 1.0 and finite.
        compression_ratio: f32,
    },
}

/// Bounded decode configuration.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct J2kDecodeOptions {
    discard_levels: u32,
    quality_layers: u32,
    strict: bool,
    max_encoded_bytes: usize,
    max_pixels: usize,
}

impl Default for J2kDecodeOptions {
    fn default() -> Self {
        Self {
            discard_levels: 0,
            quality_layers: 0,
            strict: true,
            max_encoded_bytes: DEFAULT_MAX_ENCODED_BYTES,
            max_pixels: DEFAULT_MAX_PIXELS,
        }
    }
}

impl J2kDecodeOptions {
    /// Sets the number of highest-resolution levels to discard.
    #[must_use]
    pub const fn with_discard_levels(mut self, discard_levels: u32) -> Self {
        self.discard_levels = discard_levels;
        self
    }

    /// Limits decoding to the first `quality_layers` progressive layers. Zero
    /// decodes every available layer.
    #[must_use]
    pub const fn with_quality_layers(mut self, quality_layers: u32) -> Self {
        self.quality_layers = quality_layers;
        self
    }

    /// Selects whether truncated codestreams are rejected.
    #[must_use]
    pub const fn with_strict_mode(mut self, strict: bool) -> Self {
        self.strict = strict;
        self
    }

    /// Replaces the encoded-byte and decoded-pixel limits.
    ///
    /// Zero limits are invalid and cause decode to return [`Error::Argument`].
    #[must_use]
    pub const fn with_limits(mut self, max_encoded_bytes: usize, max_pixels: usize) -> Self {
        self.max_encoded_bytes = max_encoded_bytes;
        self.max_pixels = max_pixels;
        self
    }

    /// Configured discard level.
    #[must_use]
    pub const fn discard_levels(self) -> u32 {
        self.discard_levels
    }

    /// Configured quality-layer limit.
    #[must_use]
    pub const fn quality_layers(self) -> u32 {
        self.quality_layers
    }
}

/// Encode configuration.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct J2kEncodeOptions {
    format: J2kFormat,
    compression: J2kCompression,
    max_encoded_bytes: usize,
}

impl Default for J2kEncodeOptions {
    fn default() -> Self {
        Self {
            format: J2kFormat::Codestream,
            compression: J2kCompression::Lossless,
            max_encoded_bytes: DEFAULT_MAX_ENCODED_BYTES,
        }
    }
}

impl J2kEncodeOptions {
    /// Selects raw codestream or JP2 output.
    #[must_use]
    pub const fn with_format(mut self, format: J2kFormat) -> Self {
        self.format = format;
        self
    }

    /// Selects reversible lossless or irreversible lossy compression.
    #[must_use]
    pub const fn with_compression(mut self, compression: J2kCompression) -> Self {
        self.compression = compression;
        self
    }

    /// Caps the produced codestream size.
    #[must_use]
    pub const fn with_max_encoded_bytes(mut self, max_encoded_bytes: usize) -> Self {
        self.max_encoded_bytes = max_encoded_bytes;
        self
    }
}

/// Cross-platform JPEG 2000 adapter.
///
/// The optional adapter links to BSD-2-Clause `OpenJPEG` 2.5.4 or newer. Linux
/// and macOS builds discover it with `pkg-config`; Windows MSVC builds use
/// vcpkg. Checked-in minimal bindings and all unsafe FFI are isolated in the private
/// adapter crate and no native codec type crosses this boundary.
#[derive(Clone, Copy, Debug, Default)]
#[cfg(feature = "jpeg2000")]
pub struct J2kCodec {
    decode_options: J2kDecodeOptions,
}

#[cfg(feature = "jpeg2000")]
impl J2kCodec {
    /// Creates a codec with explicit bounded decode options.
    #[must_use]
    pub const fn new(decode_options: J2kDecodeOptions) -> Self {
        Self { decode_options }
    }

    /// Decodes a raw J2K codestream or JP2 container while retaining component
    /// precision, signedness, alpha metadata, and order.
    ///
    /// The encoded-byte limit is checked before buffering to a codec stream.
    /// `OpenJPEG` header parsing does not allocate sample planes; dimensions and
    /// component counts are validated before `decode` may allocate them.
    ///
    /// # Errors
    ///
    /// Returns a typed argument or parse failure for invalid limits, oversized
    /// input/dimensions, unsupported component layouts, or malformed data.
    pub fn decode_interleaved(
        encoded: &[u8],
        options: J2kDecodeOptions,
    ) -> Result<InterleavedImage, Error> {
        if options.max_encoded_bytes == 0
            || options.max_pixels == 0
            || encoded.is_empty()
            || encoded.len() > options.max_encoded_bytes
        {
            return Err(Error::Argument);
        }
        let format = detect_format(encoded)?;
        let decoded = openjpeg::decode(
            encoded,
            backend_format(format),
            openjpeg::DecodeOptions {
                discard_levels: options.discard_levels,
                quality_layers: options.quality_layers,
                strict: options.strict,
                max_pixels: options.max_pixels.min(DEFAULT_MAX_PIXELS),
            },
        )
        .map_err(map_decode_error)?;
        backend_image_to_interleaved(decoded, options.max_pixels)
    }

    /// Decodes into the C#-compatible planar byte representation.
    ///
    /// # Errors
    ///
    /// Returns the failures documented by [`Self::decode_interleaved`] or a
    /// typed error for a component conversion/allocation failure.
    pub fn decode_bytes(encoded: &[u8], options: J2kDecodeOptions) -> Result<ManagedImage, Error> {
        interleaved_to_managed(&Self::decode_interleaved(encoded, options)?)
    }

    /// Encodes the four-component compatibility view used by `CoreJ2K`.
    ///
    /// Color images preserve RGB and optional alpha. Alpha-only images repeat
    /// alpha into RGB and encode an opaque alpha plane. Images without alpha
    /// receive opaque alpha. Bump is not a JPEG 2000 output component, matching
    /// the reference adapter.
    ///
    /// # Errors
    ///
    /// Returns a typed validation/operation failure for invalid image layouts,
    /// lossy settings, allocation/codec errors, or oversized output.
    pub fn encode(image: &ManagedImage, options: J2kEncodeOptions) -> Result<Vec<u8>, Error> {
        image.validate()?;
        if options.max_encoded_bytes == 0 {
            return Err(Error::Argument);
        }
        if let J2kCompression::Lossy { compression_ratio } = options.compression
            && (!compression_ratio.is_finite() || compression_ratio < 1.0)
        {
            return Err(Error::Argument);
        }
        encode_with_openjpeg(image, options)
    }
}

#[cfg(feature = "jpeg2000")]
impl ITextureCodec for J2kCodec {
    fn decode(&self, mut stream: Box<dyn ReadWrite + Send>) -> Result<ManagedImage, Error> {
        if self.decode_options.max_encoded_bytes == 0 {
            return Err(Error::Argument);
        }
        let limit = self
            .decode_options
            .max_encoded_bytes
            .checked_add(1)
            .ok_or(Error::Argument)?;
        let mut encoded = Vec::new();
        Read::by_ref(&mut stream)
            .take(u64::try_from(limit).map_err(|_| Error::Argument)?)
            .read_to_end(&mut encoded)
            .map_err(|_| parse("JPEG 2000 input stream"))?;
        if encoded.len() > self.decode_options.max_encoded_bytes {
            return Err(Error::Argument);
        }
        Self::decode_bytes(&encoded, self.decode_options)
    }
}

/// Cross-platform JPEG 2000 adapter implemented entirely in safe Rust.
///
/// This backend is independent from [`J2kCodec`]: enabling `rust-j2k` does not
/// link `OpenJPEG` or change the meaning of the existing native codec type.
#[derive(Clone, Copy, Debug, Default)]
#[cfg(feature = "rust-j2k")]
pub struct RustJ2kCodec {
    decode_options: J2kDecodeOptions,
}

#[cfg(feature = "rust-j2k")]
impl RustJ2kCodec {
    /// Creates a codec with explicit bounded decode options.
    #[must_use]
    pub const fn new(decode_options: J2kDecodeOptions) -> Self {
        Self { decode_options }
    }

    /// Decodes raw J2K/J2C or JP2 into native component planes.
    ///
    /// Header geometry, sampling, and component count are checked before the
    /// backend is allowed to allocate decoded sample storage.
    ///
    /// # Errors
    ///
    /// Returns a typed argument or parse failure for invalid limits,
    /// unsupported component geometry, malformed input, or codec failure.
    pub fn decode_interleaved(
        encoded: &[u8],
        options: J2kDecodeOptions,
    ) -> Result<InterleavedImage, Error> {
        validate_decode_input(encoded, options)?;
        detect_format(encoded)?;
        let prepared = prepare_rust_decode_input(encoded, options)?;
        let encoded = prepared.as_ref();

        let settings = if options.strict {
            rust_j2k::DecodeSettings::strict()
        } else {
            rust_j2k::DecodeSettings::lenient()
        };
        let support =
            rust_j2k::J2kDecoder::inspect_support(encoded).map_err(map_rust_decode_error)?;
        let (width, height) = checked_dimensions(
            support.info.dimensions.0,
            support.info.dimensions.1,
            options.max_pixels,
        )?;
        if !(1..=5).contains(&support.component_count()) || support.has_component_subsampling() {
            return Err(Error::Argument);
        }
        if options.discard_levels == 0 {
            let mut decoder = rust_j2k::J2kDecoder::new_with_settings(encoded, settings)
                .map_err(map_rust_decode_error)?;
            let native = decoder
                .decode_native_components()
                .map_err(map_rust_decode_error)?;
            return rust_native_to_interleaved(&native, width, height, options.max_pixels);
        }

        decode_rust_reduced(encoded, settings, &support, options)
    }

    /// Decodes into the C#-compatible planar byte representation.
    ///
    /// # Errors
    ///
    /// Returns the failures documented by [`Self::decode_interleaved`] or a
    /// typed component conversion/allocation failure.
    pub fn decode_bytes(encoded: &[u8], options: J2kDecodeOptions) -> Result<ManagedImage, Error> {
        interleaved_to_managed(&Self::decode_interleaved(encoded, options)?)
    }

    /// Encodes the four-component `CoreJ2K` compatibility view as J2K or JP2.
    ///
    /// # Errors
    ///
    /// Returns a typed validation/operation failure for invalid image layouts,
    /// lossy settings, codec errors, allocation failure, or oversized output.
    pub fn encode(image: &ManagedImage, options: J2kEncodeOptions) -> Result<Vec<u8>, Error> {
        validate_encode_request(image, options)?;
        encode_with_rust_j2k(image, options)
    }
}

#[cfg(feature = "rust-j2k")]
impl ITextureCodec for RustJ2kCodec {
    fn decode(&self, mut stream: Box<dyn ReadWrite + Send>) -> Result<ManagedImage, Error> {
        read_bounded_stream(&mut stream, self.decode_options.max_encoded_bytes)
            .and_then(|encoded| Self::decode_bytes(&encoded, self.decode_options))
    }
}

#[cfg(feature = "rust-j2k")]
fn validate_decode_input(encoded: &[u8], options: J2kDecodeOptions) -> Result<(), Error> {
    if options.max_encoded_bytes == 0
        || options.max_pixels == 0
        || encoded.is_empty()
        || encoded.len() > options.max_encoded_bytes
    {
        return Err(Error::Argument);
    }
    Ok(())
}

#[cfg(feature = "rust-j2k")]
fn validate_encode_request(image: &ManagedImage, options: J2kEncodeOptions) -> Result<(), Error> {
    image.validate()?;
    if options.max_encoded_bytes == 0 {
        return Err(Error::Argument);
    }
    if let J2kCompression::Lossy { compression_ratio } = options.compression
        && (!compression_ratio.is_finite() || compression_ratio < 1.0)
    {
        return Err(Error::Argument);
    }
    Ok(())
}

#[cfg(feature = "rust-j2k")]
fn read_bounded_stream(
    stream: &mut (dyn ReadWrite + Send),
    max_encoded_bytes: usize,
) -> Result<Vec<u8>, Error> {
    if max_encoded_bytes == 0 {
        return Err(Error::Argument);
    }
    let limit = max_encoded_bytes.checked_add(1).ok_or(Error::Argument)?;
    let mut encoded = Vec::new();
    Read::take(stream, u64::try_from(limit).map_err(|_| Error::Argument)?)
        .read_to_end(&mut encoded)
        .map_err(|_| parse("JPEG 2000 input stream"))?;
    if encoded.len() > max_encoded_bytes {
        return Err(Error::Argument);
    }
    Ok(encoded)
}

#[cfg(feature = "jpeg2000")]
fn backend_image_to_interleaved(
    image: openjpeg::Image,
    max_pixels: usize,
) -> Result<InterleavedImage, Error> {
    let (width, height) = checked_dimensions(image.width, image.height, max_pixels)?;
    let mut decoded = Vec::new();
    decoded
        .try_reserve_exact(image.components.len())
        .map_err(|_| Error::InvalidOperation)?;
    for component in image.components {
        if component.width != image.width || component.height != image.height {
            return Err(parse("subsampled JPEG 2000 components"));
        }
        decoded.push(InterleavedComponent::new(
            component.precision,
            component.signed,
            component.alpha,
            component.samples,
        )?);
    }
    InterleavedImage::new(width, height, decoded)
}

#[cfg(feature = "rust-j2k")]
fn rust_native_to_interleaved(
    image: &rust_j2k::J2kDecodedNativeComponents,
    width: i32,
    height: i32,
    max_pixels: usize,
) -> Result<InterleavedImage, Error> {
    let dimensions = image.dimensions();
    let checked = checked_dimensions(dimensions.0, dimensions.1, max_pixels)?;
    if checked != (width, height) || !(1..=5).contains(&image.planes().len()) {
        return Err(Error::Argument);
    }
    let has_alpha = image.has_alpha();
    let plane_count = image.planes().len();
    let mut components = Vec::new();
    components
        .try_reserve_exact(plane_count)
        .map_err(|_| Error::InvalidOperation)?;
    for (index, plane) in image.planes().iter().enumerate() {
        if plane.dimensions() != dimensions || plane.sampling() != (1, 1) {
            return Err(parse("subsampled JPEG 2000 components"));
        }
        let samples = unpack_native_samples(
            plane.data(),
            plane.bytes_per_sample(),
            plane.bit_depth(),
            plane.signed(),
        )?;
        components.push(InterleavedComponent::new(
            plane.bit_depth(),
            plane.signed(),
            has_alpha && index + 1 == plane_count,
            samples,
        )?);
    }
    InterleavedImage::new(width, height, components)
}

#[cfg(feature = "rust-j2k")]
fn unpack_native_samples(
    data: &[u8],
    bytes_per_sample: u8,
    precision: u8,
    signed: bool,
) -> Result<Vec<i32>, Error> {
    let sample_width = usize::from(bytes_per_sample);
    if sample_width == 0 || sample_width > 4 || !data.len().is_multiple_of(sample_width) {
        return Err(Error::Argument);
    }
    let mut samples = Vec::new();
    samples
        .try_reserve_exact(data.len() / sample_width)
        .map_err(|_| Error::InvalidOperation)?;
    for bytes in data.chunks_exact(sample_width) {
        let mut packed = [0_u8; 4];
        packed[..sample_width].copy_from_slice(bytes);
        let raw = u32::from_le_bytes(packed);
        let value = if signed {
            let shift = 32_u32
                .checked_sub(u32::from(precision))
                .ok_or(Error::Argument)?;
            i32::from_ne_bytes((raw << shift).to_ne_bytes()) >> shift
        } else {
            i32::try_from(raw).map_err(|_| Error::Argument)?
        };
        samples.push(value);
    }
    Ok(samples)
}

#[cfg(feature = "rust-j2k")]
fn decode_rust_reduced(
    encoded: &[u8],
    settings: rust_j2k::DecodeSettings,
    support: &rust_j2k::J2kSupportInfo,
    options: J2kDecodeOptions,
) -> Result<InterleavedImage, Error> {
    if u32::from(support.info.resolution_levels) <= options.discard_levels {
        return decode_rust_reduced_native(encoded, settings, support, options);
    }
    let scale = match options.discard_levels {
        1 => rust_j2k::Downscale::Half,
        2 => rust_j2k::Downscale::Quarter,
        3 => rust_j2k::Downscale::Eighth,
        _ => return decode_rust_reduced_native(encoded, settings, support, options),
    };
    let components = usize::from(support.component_count());
    if !matches!(components, 1 | 3 | 4) {
        return decode_rust_reduced_native(encoded, settings, support, options);
    }
    let denominator = 1_u32
        .checked_shl(options.discard_levels)
        .ok_or(Error::Argument)?;
    let reduced_width = support.info.dimensions.0.div_ceil(denominator);
    let reduced_height = support.info.dimensions.1.div_ceil(denominator);
    let (width, height) = checked_dimensions(reduced_width, reduced_height, options.max_pixels)?;
    let pixels = usize::try_from(reduced_width)
        .ok()
        .and_then(|value| {
            usize::try_from(reduced_height)
                .ok()
                .and_then(|height| value.checked_mul(height))
        })
        .ok_or(Error::Argument)?;
    let length = pixels.checked_mul(components).ok_or(Error::Argument)?;
    let mut packed = Vec::new();
    packed
        .try_reserve_exact(length)
        .map_err(|_| Error::InvalidOperation)?;
    packed.resize(length, 0);
    let stride = usize::try_from(reduced_width)
        .ok()
        .and_then(|width| width.checked_mul(components))
        .ok_or(Error::Argument)?;
    let format = match components {
        1 => rust_j2k::PixelFormat::Gray8,
        3 => rust_j2k::PixelFormat::Rgb8,
        4 => rust_j2k::PixelFormat::Rgba8,
        _ => return Err(Error::Argument),
    };
    let mut decoder = rust_j2k::J2kDecoder::new_with_settings(encoded, settings)
        .map_err(map_rust_decode_error)?;
    decoder
        .decode_scaled_into(
            &mut rust_j2k::J2kScratchPool::new(),
            &mut packed,
            stride,
            format,
            scale,
        )
        .map_err(map_rust_decode_error)?;
    packed_bytes_to_interleaved(width, height, components, &packed)
}

#[cfg(feature = "rust-j2k")]
fn decode_rust_reduced_native(
    encoded: &[u8],
    settings: rust_j2k::DecodeSettings,
    support: &rust_j2k::J2kSupportInfo,
    options: J2kDecodeOptions,
) -> Result<InterleavedImage, Error> {
    let denominator = 1_u32
        .checked_shl(options.discard_levels)
        .ok_or(Error::Argument)?;
    let reduced_width = support.info.dimensions.0.div_ceil(denominator);
    let reduced_height = support.info.dimensions.1.div_ceil(denominator);
    let (width, height) = checked_dimensions(reduced_width, reduced_height, options.max_pixels)?;
    let mut decoder = rust_j2k::J2kDecoder::new_with_settings(encoded, settings)
        .map_err(map_rust_decode_error)?;
    let native = decoder
        .decode_native_components()
        .map_err(map_rust_decode_error)?;
    let has_alpha = native.has_alpha();
    let count = native.planes().len();
    let mut components = Vec::new();
    components
        .try_reserve_exact(count)
        .map_err(|_| Error::InvalidOperation)?;
    for (index, plane) in native.planes().iter().enumerate() {
        if plane.dimensions() != support.info.dimensions || plane.sampling() != (1, 1) {
            return Err(parse("subsampled JPEG 2000 components"));
        }
        let full = unpack_native_samples(
            plane.data(),
            plane.bytes_per_sample(),
            plane.bit_depth(),
            plane.signed(),
        )?;
        let reduced = subsample_plane(
            &full,
            support.info.dimensions,
            (reduced_width, reduced_height),
            denominator,
        )?;
        components.push(InterleavedComponent::new(
            plane.bit_depth(),
            plane.signed(),
            has_alpha && index + 1 == count,
            reduced,
        )?);
    }
    InterleavedImage::new(width, height, components)
}

#[cfg(feature = "rust-j2k")]
fn subsample_plane(
    samples: &[i32],
    source: (u32, u32),
    target: (u32, u32),
    denominator: u32,
) -> Result<Vec<i32>, Error> {
    let source_width = usize::try_from(source.0).map_err(|_| Error::Argument)?;
    let expected = source_width
        .checked_mul(usize::try_from(source.1).map_err(|_| Error::Argument)?)
        .ok_or(Error::Argument)?;
    if samples.len() != expected {
        return Err(Error::Argument);
    }
    let target_len = usize::try_from(target.0)
        .ok()
        .and_then(|width| {
            usize::try_from(target.1)
                .ok()
                .and_then(|height| width.checked_mul(height))
        })
        .ok_or(Error::Argument)?;
    let mut reduced = Vec::new();
    reduced
        .try_reserve_exact(target_len)
        .map_err(|_| Error::InvalidOperation)?;
    for y in 0..target.1 {
        let source_y = y.checked_mul(denominator).ok_or(Error::Argument)?;
        for x in 0..target.0 {
            let source_x = x.checked_mul(denominator).ok_or(Error::Argument)?;
            let index = usize::try_from(source_y)
                .ok()
                .and_then(|y| y.checked_mul(source_width))
                .and_then(|row| {
                    usize::try_from(source_x)
                        .ok()
                        .and_then(|x| row.checked_add(x))
                })
                .ok_or(Error::Argument)?;
            reduced.push(*samples.get(index).ok_or(Error::Argument)?);
        }
    }
    Ok(reduced)
}

#[cfg(feature = "rust-j2k")]
fn packed_bytes_to_interleaved(
    width: i32,
    height: i32,
    component_count: usize,
    packed: &[u8],
) -> Result<InterleavedImage, Error> {
    let pixels = usize::try_from(width)
        .ok()
        .and_then(|width| {
            usize::try_from(height)
                .ok()
                .and_then(|height| width.checked_mul(height))
        })
        .ok_or(Error::Argument)?;
    if packed.len() != pixels.checked_mul(component_count).ok_or(Error::Argument)? {
        return Err(Error::Argument);
    }
    let mut components = Vec::new();
    components
        .try_reserve_exact(component_count)
        .map_err(|_| Error::InvalidOperation)?;
    for component in 0..component_count {
        let mut samples = Vec::new();
        samples
            .try_reserve_exact(pixels)
            .map_err(|_| Error::InvalidOperation)?;
        samples.extend(
            packed
                .iter()
                .skip(component)
                .step_by(component_count)
                .map(|sample| i32::from(*sample)),
        );
        components.push(InterleavedComponent::new(
            8,
            false,
            component + 1 == component_count && matches!(component_count, 2 | 4 | 5),
            samples,
        )?);
    }
    InterleavedImage::new(width, height, components)
}

fn interleaved_to_managed(image: &InterleavedImage) -> Result<ManagedImage, Error> {
    let channels = channels_for_components(image.number_of_components())?;
    let pixels = usize::try_from(image.width())
        .ok()
        .and_then(|width| {
            usize::try_from(image.height())
                .ok()
                .and_then(|height| width.checked_mul(height))
        })
        .ok_or(Error::Argument)?;
    let mut bytes = Vec::new();
    let length = pixels
        .checked_mul(image.number_of_components())
        .ok_or(Error::Argument)?;
    bytes
        .try_reserve_exact(length)
        .map_err(|_| Error::InvalidOperation)?;
    bytes.resize(length, 0);
    let mut plane = vec![0; pixels];
    for component in 0..image.number_of_components() {
        image.to_component_bytes(component, &mut plane)?;
        for (pixel, sample) in plane.iter().enumerate() {
            bytes[pixel * image.number_of_components() + component] = *sample;
        }
    }
    managed_from_reference_interleaved(image.width(), image.height(), channels, &bytes)
}

#[cfg(feature = "jpeg2000")]
fn encode_with_openjpeg(image: &ManagedImage, options: J2kEncodeOptions) -> Result<Vec<u8>, Error> {
    let width = u32::try_from(image.width).map_err(|_| Error::Argument)?;
    let height = u32::try_from(image.height).map_err(|_| Error::Argument)?;
    let planes = reference_encode_planes(image)?;
    let components = [
        openjpeg::ComponentRef {
            precision: 8,
            signed: false,
            alpha: false,
            samples: &planes[0],
        },
        openjpeg::ComponentRef {
            precision: 8,
            signed: false,
            alpha: false,
            samples: &planes[1],
        },
        openjpeg::ComponentRef {
            precision: 8,
            signed: false,
            alpha: false,
            samples: &planes[2],
        },
        openjpeg::ComponentRef {
            precision: 8,
            signed: false,
            alpha: true,
            samples: &planes[3],
        },
    ];
    let compression = match options.compression {
        J2kCompression::Lossless => openjpeg::Compression::Lossless,
        J2kCompression::Lossy { compression_ratio } => {
            openjpeg::Compression::Lossy { compression_ratio }
        }
    };
    openjpeg::encode(
        width,
        height,
        &components,
        backend_format(options.format),
        compression,
        options.max_encoded_bytes,
    )
    .map_err(map_encode_error)
}

#[cfg(feature = "rust-j2k")]
fn encode_with_rust_j2k(image: &ManagedImage, options: J2kEncodeOptions) -> Result<Vec<u8>, Error> {
    let width = u32::try_from(image.width).map_err(|_| Error::Argument)?;
    let height = u32::try_from(image.height).map_err(|_| Error::Argument)?;
    checked_dimensions(width, height, DEFAULT_MAX_PIXELS)?;
    let planes = reference_encode_planes(image)?;
    let pixels = 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 length = pixels.checked_mul(4).ok_or(Error::Argument)?;
    let mut interleaved = Vec::new();
    interleaved
        .try_reserve_exact(length)
        .map_err(|_| Error::InvalidOperation)?;
    for pixel in 0..pixels {
        for plane in &planes {
            interleaved.push(u8::try_from(plane[pixel]).map_err(|_| Error::Argument)?);
        }
    }

    let codestream = match options.compression {
        J2kCompression::Lossless => {
            let samples =
                rust_j2k::J2kLosslessSamples::new(&interleaved, width, height, 4, 8, false)
                    .map_err(map_rust_encode_error)?;
            rust_j2k::encode_j2k_lossless(
                samples,
                &rust_j2k::J2kLosslessEncodeOptions::default()
                    .with_cpu_only_backend()
                    .with_max_decomposition_levels(Some(6)),
            )
            .map_err(map_rust_encode_error)?
            .codestream
        }
        J2kCompression::Lossy { compression_ratio } => {
            let target_bytes = lossy_target_bytes(length, compression_ratio)?;
            let layers = [
                rust_j2k::J2kQualityLayer::new(rust_j2k::J2kRateTarget::Bytes(
                    target_bytes.div_ceil(3),
                )),
                rust_j2k::J2kQualityLayer::new(rust_j2k::J2kRateTarget::Bytes(
                    target_bytes.saturating_mul(2).div_ceil(3),
                )),
                rust_j2k::J2kQualityLayer::new(rust_j2k::J2kRateTarget::Bytes(target_bytes)),
            ];
            let samples = rust_j2k::J2kLossySamples::new(&interleaved, width, height, 4, 8, false)
                .map_err(map_rust_encode_error)?;
            rust_j2k::encode_j2k_lossy(
                samples,
                &rust_j2k::J2kLossyEncodeOptions::default()
                    .with_cpu_only_backend()
                    .with_max_decomposition_levels(Some(6))
                    .with_quality_layers(layers.to_vec())
                    .with_marker_segments(vec![rust_j2k::J2kMarkerSegment::Plt]),
            )
            .map_err(map_rust_encode_error)?
            .codestream
        }
    };

    let encoded = match options.format {
        J2kFormat::Codestream => codestream,
        J2kFormat::Jp2 => {
            rust_j2k::wrap_j2k_codestream(&codestream, rust_j2k::J2kFileWrapOptions::jp2())
                .map_err(map_rust_encode_error)?
        }
    };
    if encoded.len() > options.max_encoded_bytes {
        return Err(Error::Argument);
    }
    Ok(encoded)
}

#[cfg(feature = "rust-j2k")]
fn prepare_rust_decode_input(
    encoded: &[u8],
    options: J2kDecodeOptions,
) -> Result<Cow<'_, [u8]>, Error> {
    let format = detect_format(encoded)?;
    let container = if !options.strict
        && format == J2kFormat::Jp2
        && rust_j2k::extract_j2k_codestream_payload(encoded).is_err()
    {
        let mut repaired = Vec::new();
        repaired
            .try_reserve_exact(encoded.len().saturating_add(2))
            .map_err(|_| Error::InvalidOperation)?;
        repaired.extend_from_slice(encoded);
        repaired.extend_from_slice(&[0xff, 0xd9]);
        Cow::Owned(repaired)
    } else {
        Cow::Borrowed(encoded)
    };
    let payload = rust_j2k::extract_j2k_codestream_payload(container.as_ref())
        .map_err(map_rust_decode_error)?
        .codestream();
    let available = codestream_quality_layers(payload)?;
    if options.quality_layers != 0 && options.quality_layers < u32::from(available) {
        let requested = u16::try_from(options.quality_layers).map_err(|_| Error::Argument)?;
        let limited = limit_codestream_quality_layers(payload, available, requested)?;
        if format == J2kFormat::Jp2 {
            return rust_j2k::wrap_j2k_codestream(&limited, rust_j2k::J2kFileWrapOptions::jp2())
                .map(Cow::Owned)
                .map_err(map_rust_decode_error);
        }
        return Ok(Cow::Owned(limited));
    }
    if !options.strict {
        if payload.ends_with(&[0xff, 0xd9]) {
            return Ok(container);
        }
        let mut repaired = Vec::new();
        repaired
            .try_reserve_exact(payload.len().saturating_add(2))
            .map_err(|_| Error::InvalidOperation)?;
        repaired.extend_from_slice(payload);
        repaired.extend_from_slice(&[0xff, 0xd9]);
        if format == J2kFormat::Jp2 {
            return rust_j2k::wrap_j2k_codestream(&repaired, rust_j2k::J2kFileWrapOptions::jp2())
                .map(Cow::Owned)
                .map_err(map_rust_decode_error);
        }
        return Ok(Cow::Owned(repaired));
    }
    Ok(Cow::Borrowed(encoded))
}

#[cfg(feature = "rust-j2k")]
fn limit_codestream_quality_layers(
    codestream: &[u8],
    available: u16,
    requested: u16,
) -> Result<Vec<u8>, Error> {
    if requested == 0 || requested >= available {
        return Err(Error::Argument);
    }
    let main = parse_main_header(codestream)?;
    if main.progression != 0 || main.has_progression_changes {
        return Err(Error::InvalidOperation);
    }
    let mut output = Vec::new();
    output
        .try_reserve_exact(codestream.len())
        .map_err(|_| Error::InvalidOperation)?;
    output.extend_from_slice(&codestream[..main.first_tile_part]);
    output[main.cod_layers_offset..main.cod_layers_offset + 2]
        .copy_from_slice(&requested.to_be_bytes());

    let mut tile_offset = main.first_tile_part;
    while tile_offset
        .checked_add(2)
        .is_some_and(|end| end <= codestream.len())
        && codestream[tile_offset..].starts_with(&[0xff, 0x90])
    {
        let consumed =
            append_limited_tile_part(codestream, tile_offset, available, requested, &mut output)?;
        tile_offset = tile_offset.checked_add(consumed).ok_or(Error::Argument)?;
    }
    if tile_offset
        .checked_add(2)
        .is_none_or(|end| end > codestream.len())
    {
        return Err(parse("JPEG 2000 EOC marker"));
    }
    output.extend_from_slice(&[0xff, 0xd9]);
    Ok(output)
}

#[cfg(feature = "rust-j2k")]
#[derive(Clone, Copy)]
struct MainHeader {
    first_tile_part: usize,
    cod_layers_offset: usize,
    progression: u8,
    has_progression_changes: bool,
}

#[cfg(feature = "rust-j2k")]
fn parse_main_header(codestream: &[u8]) -> Result<MainHeader, Error> {
    if !codestream.starts_with(&[0xff, 0x4f]) {
        return Err(parse("JPEG 2000 codestream"));
    }
    let mut offset = 2_usize;
    let mut cod = None;
    let mut has_progression_changes = false;
    while offset
        .checked_add(2)
        .is_some_and(|end| end <= codestream.len())
    {
        if codestream[offset] != 0xff {
            return Err(parse("JPEG 2000 main header marker"));
        }
        let marker = codestream[offset + 1];
        if marker == 0x90 {
            let (cod_layers_offset, progression) =
                cod.ok_or_else(|| parse("JPEG 2000 COD marker"))?;
            return Ok(MainHeader {
                first_tile_part: offset,
                cod_layers_offset,
                progression,
                has_progression_changes,
            });
        }
        let (segment_length, end) = marker_segment_bounds(codestream, offset)?;
        if marker == 0x52 {
            if segment_length < 7 {
                return Err(parse("JPEG 2000 COD marker"));
            }
            cod = Some((offset + 6, codestream[offset + 5]));
        } else if marker == 0x5f {
            has_progression_changes = true;
        }
        offset = end;
    }
    Err(parse("JPEG 2000 tile part"))
}

#[cfg(feature = "rust-j2k")]
fn append_limited_tile_part(
    codestream: &[u8],
    tile_offset: usize,
    available: u16,
    requested: u16,
    output: &mut Vec<u8>,
) -> Result<usize, Error> {
    if tile_offset
        .checked_add(12)
        .is_none_or(|end| end > codestream.len())
    {
        return Err(parse("JPEG 2000 SOT marker"));
    }
    let tile_length = usize::try_from(u32::from_be_bytes([
        codestream[tile_offset + 6],
        codestream[tile_offset + 7],
        codestream[tile_offset + 8],
        codestream[tile_offset + 9],
    ]))
    .map_err(|_| Error::Argument)?;
    if tile_length < 14
        || tile_offset
            .checked_add(tile_length)
            .is_none_or(|end| end > codestream.len())
    {
        return Err(parse("JPEG 2000 tile-part length"));
    }
    let tile_end = tile_offset + tile_length;
    let mut header_offset = tile_offset + 12;
    let output_start = output.len();
    output.extend_from_slice(&codestream[tile_offset..tile_offset + 12]);
    let mut packet_lengths = Vec::new();
    loop {
        if header_offset
            .checked_add(2)
            .is_none_or(|end| end > tile_end)
            || codestream[header_offset] != 0xff
        {
            return Err(parse("JPEG 2000 tile header"));
        }
        let marker = codestream[header_offset + 1];
        if marker == 0x93 {
            output.extend_from_slice(&[0xff, 0x93]);
            header_offset += 2;
            break;
        }
        let (_, marker_end) = marker_segment_bounds(codestream, header_offset)?;
        if marker_end > tile_end {
            return Err(parse("JPEG 2000 tile header"));
        }
        if marker == 0x58 {
            decode_plt_lengths(
                &codestream[header_offset + 5..marker_end],
                &mut packet_lengths,
            )?;
        } else {
            if marker == 0x5f {
                return Err(Error::InvalidOperation);
            }
            output.extend_from_slice(&codestream[header_offset..marker_end]);
        }
        header_offset = marker_end;
    }
    if packet_lengths.is_empty() || packet_lengths.len() % usize::from(available) != 0 {
        return Err(Error::InvalidOperation);
    }
    let packets_per_layer = packet_lengths.len() / usize::from(available);
    let keep_packets = packets_per_layer
        .checked_mul(usize::from(requested))
        .ok_or(Error::Argument)?;
    let keep_bytes = packet_lengths[..keep_packets]
        .iter()
        .try_fold(0_usize, |total, length| {
            total.checked_add(*length).ok_or(Error::Argument)
        })?;
    if header_offset
        .checked_add(keep_bytes)
        .is_none_or(|end| end > tile_end)
    {
        return Err(parse("JPEG 2000 packet lengths"));
    }
    output.extend_from_slice(&codestream[header_offset..header_offset + keep_bytes]);
    let limited_length = output
        .len()
        .checked_sub(output_start)
        .ok_or(Error::Argument)?;
    let limited_length = u32::try_from(limited_length).map_err(|_| Error::Argument)?;
    output[output_start + 6..output_start + 10].copy_from_slice(&limited_length.to_be_bytes());
    Ok(tile_length)
}

#[cfg(feature = "rust-j2k")]
fn decode_plt_lengths(data: &[u8], lengths: &mut Vec<usize>) -> Result<(), Error> {
    let mut value = 0_usize;
    let mut pending = false;
    for byte in data {
        value = value
            .checked_shl(7)
            .and_then(|value| value.checked_add(usize::from(byte & 0x7f)))
            .ok_or(Error::Argument)?;
        pending = byte & 0x80 != 0;
        if !pending {
            lengths
                .try_reserve(1)
                .map_err(|_| Error::InvalidOperation)?;
            lengths.push(value);
            value = 0;
        }
    }
    if pending {
        return Err(parse("JPEG 2000 PLT packet length"));
    }
    Ok(())
}

#[cfg(feature = "rust-j2k")]
fn marker_segment_bounds(bytes: &[u8], offset: usize) -> Result<(usize, usize), Error> {
    if offset.checked_add(4).is_none_or(|end| end > bytes.len()) {
        return Err(parse("JPEG 2000 marker length"));
    }
    let segment_length = usize::from(u16::from_be_bytes([bytes[offset + 2], bytes[offset + 3]]));
    if segment_length < 2 {
        return Err(parse("JPEG 2000 marker length"));
    }
    let end = offset
        .checked_add(2)
        .and_then(|value| value.checked_add(segment_length))
        .filter(|end| *end <= bytes.len())
        .ok_or_else(|| parse("JPEG 2000 marker length"))?;
    Ok((segment_length, end))
}

#[cfg(feature = "rust-j2k")]
fn codestream_quality_layers(codestream: &[u8]) -> Result<u16, Error> {
    if !codestream.starts_with(&[0xff, 0x4f]) {
        return Err(parse("JPEG 2000 codestream"));
    }
    let mut offset = 2_usize;
    while offset
        .checked_add(4)
        .is_some_and(|end| end <= codestream.len())
    {
        if codestream[offset] != 0xff {
            return Err(parse("JPEG 2000 marker"));
        }
        let marker = codestream[offset + 1];
        if matches!(marker, 0x90 | 0x93 | 0xd9) {
            break;
        }
        let segment_length = usize::from(u16::from_be_bytes([
            codestream[offset + 2],
            codestream[offset + 3],
        ]));
        if segment_length < 2 {
            return Err(parse("JPEG 2000 marker length"));
        }
        let end = offset
            .checked_add(2)
            .and_then(|value| value.checked_add(segment_length))
            .filter(|end| *end <= codestream.len())
            .ok_or_else(|| parse("JPEG 2000 marker length"))?;
        if marker == 0x52 {
            if segment_length < 7 {
                return Err(parse("JPEG 2000 COD marker"));
            }
            let layers = u16::from_be_bytes([codestream[offset + 6], codestream[offset + 7]]);
            return (layers != 0)
                .then_some(layers)
                .ok_or_else(|| parse("JPEG 2000 quality layers"));
        }
        offset = end;
    }
    Err(parse("JPEG 2000 COD marker"))
}

fn reference_encode_planes(image: &ManagedImage) -> Result<[Vec<i32>; 4], Error> {
    let pixels = usize::try_from(image.width)
        .ok()
        .and_then(|width| {
            usize::try_from(image.height)
                .ok()
                .and_then(|height| width.checked_mul(height))
        })
        .ok_or(Error::Argument)?;
    let mut planes = [Vec::new(), Vec::new(), Vec::new(), Vec::new()];
    for plane in &mut planes {
        plane
            .try_reserve_exact(pixels)
            .map_err(|_| Error::InvalidOperation)?;
        plane.resize(pixels, 0);
    }
    let has_color = image.channels.contains(ManagedImageImageChannels::COLOR);
    let has_alpha = image.channels.contains(ManagedImageImageChannels::ALPHA);

    if has_alpha && !has_color {
        for (pixel, sample) in image.alpha.iter().copied().enumerate() {
            let sample = i32::from(sample);
            planes[0][pixel] = sample;
            planes[1][pixel] = sample;
            planes[2][pixel] = sample;
            planes[3][pixel] = 255;
        }
        return Ok(planes);
    }
    if !has_color {
        return Err(Error::InvalidOperation);
    }
    for (pixel, red) in image.red.iter().copied().enumerate() {
        planes[0][pixel] = i32::from(red);
        planes[1][pixel] = i32::from(image.green[pixel]);
        planes[2][pixel] = i32::from(image.blue[pixel]);
        planes[3][pixel] = if has_alpha {
            i32::from(image.alpha[pixel])
        } else {
            255
        };
    }
    Ok(planes)
}

fn managed_from_reference_interleaved(
    width: i32,
    height: i32,
    channels: ManagedImageImageChannels,
    bytes: &[u8],
) -> Result<ManagedImage, Error> {
    let components = component_count(channels);
    let pixels = usize::try_from(width)
        .ok()
        .and_then(|width| {
            usize::try_from(height)
                .ok()
                .and_then(|height| width.checked_mul(height))
        })
        .ok_or(Error::Argument)?;
    if bytes.len() != pixels.checked_mul(components).ok_or(Error::Argument)? {
        return Err(Error::Argument);
    }
    let mut image = ManagedImage::new(width, height, channels)?;
    for pixel in 0..pixels {
        let source = pixel * components;
        match components {
            1 => image.red[pixel] = bytes[source],
            2 => {
                image.red[pixel] = bytes[source];
                image.alpha[pixel] = bytes[source + 1];
            }
            3 => {
                image.red[pixel] = bytes[source];
                image.green[pixel] = bytes[source + 1];
                image.blue[pixel] = bytes[source + 2];
            }
            4 => {
                image.red[pixel] = bytes[source];
                image.green[pixel] = bytes[source + 1];
                image.blue[pixel] = bytes[source + 2];
                image.alpha[pixel] = bytes[source + 3];
            }
            5 => {
                image.red[pixel] = bytes[source];
                image.green[pixel] = bytes[source + 1];
                image.blue[pixel] = bytes[source + 2];
                image.bump[pixel] = bytes[source + 3];
                image.alpha[pixel] = bytes[source + 4];
            }
            _ => return Err(Error::Argument),
        }
    }
    Ok(image)
}

fn channels_for_components(components: usize) -> Result<ManagedImageImageChannels, Error> {
    Ok(match components {
        1 => ManagedImageImageChannels::GRAY,
        2 => ManagedImageImageChannels::GRAY | ManagedImageImageChannels::ALPHA,
        3 => ManagedImageImageChannels::COLOR,
        4 => ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA,
        5 => {
            ManagedImageImageChannels::COLOR
                | ManagedImageImageChannels::BUMP
                | ManagedImageImageChannels::ALPHA
        }
        _ => return Err(Error::Argument),
    })
}

fn component_count(channels: ManagedImageImageChannels) -> usize {
    if channels.contains(ManagedImageImageChannels::BUMP) {
        5
    } else if channels.contains(ManagedImageImageChannels::COLOR) {
        3 + usize::from(channels.contains(ManagedImageImageChannels::ALPHA))
    } else if channels.contains(ManagedImageImageChannels::GRAY) {
        1 + usize::from(channels.contains(ManagedImageImageChannels::ALPHA))
    } else {
        usize::from(channels.contains(ManagedImageImageChannels::ALPHA))
    }
}

fn detect_format(encoded: &[u8]) -> Result<J2kFormat, Error> {
    const JP2_MAGIC: &[u8] = &[
        0x00, 0x00, 0x00, 0x0c, b'j', b'P', b' ', b' ', 0x0d, 0x0a, 0x87, 0x0a,
    ];
    const J2K_MAGIC: &[u8] = &[0xff, 0x4f, 0xff, 0x51];
    if encoded.starts_with(JP2_MAGIC) {
        Ok(J2kFormat::Jp2)
    } else if encoded.starts_with(J2K_MAGIC) {
        Ok(J2kFormat::Codestream)
    } else {
        Err(parse("JPEG 2000 magic"))
    }
}

#[cfg(feature = "jpeg2000")]
const fn backend_format(format: J2kFormat) -> openjpeg::Format {
    match format {
        J2kFormat::Codestream => openjpeg::Format::J2k,
        J2kFormat::Jp2 => openjpeg::Format::Jp2,
    }
}

#[cfg(feature = "jpeg2000")]
const fn map_decode_error(error: openjpeg::Error) -> Error {
    match error {
        openjpeg::Error::LimitExceeded => Error::Argument,
        openjpeg::Error::Allocation => Error::InvalidOperation,
        openjpeg::Error::InvalidInput | openjpeg::Error::Codec => parse("JPEG 2000 codestream"),
        _ => parse("JPEG 2000 backend"),
    }
}

#[cfg(feature = "jpeg2000")]
const fn map_encode_error(error: openjpeg::Error) -> Error {
    match error {
        openjpeg::Error::InvalidInput | openjpeg::Error::LimitExceeded => Error::Argument,
        _ => Error::InvalidOperation,
    }
}

#[cfg(feature = "rust-j2k")]
#[allow(
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss,
    reason = "validated finite positive ratio and a u32-bounded byte count make this conversion exact enough for rate targeting"
)]
fn lossy_target_bytes(length: usize, compression_ratio: f32) -> Result<u64, Error> {
    let length = u32::try_from(length).map_err(|_| Error::Argument)?;
    Ok((f64::from(length) / f64::from(compression_ratio))
        .round()
        .max(1.0) as u64)
}

#[cfg(feature = "rust-j2k")]
#[allow(
    clippy::needless_pass_by_value,
    reason = "map_err supplies owned non-Copy backend errors and the public error intentionally does not retain backend types"
)]
fn map_rust_decode_error(error: rust_j2k::J2kError) -> Error {
    match error {
        rust_j2k::J2kError::Buffer(_)
        | rust_j2k::J2kError::InvalidSamples { .. }
        | rust_j2k::J2kError::InvalidRegion { .. }
        | rust_j2k::J2kError::DimensionOverflow { .. } => Error::Argument,
        _ => parse("JPEG 2000 codestream"),
    }
}

#[cfg(feature = "rust-j2k")]
#[allow(
    clippy::needless_pass_by_value,
    reason = "map_err supplies owned non-Copy backend errors and the public error intentionally does not retain backend types"
)]
fn map_rust_encode_error(error: rust_j2k::J2kError) -> Error {
    match error {
        rust_j2k::J2kError::Buffer(_)
        | rust_j2k::J2kError::InvalidSamples { .. }
        | rust_j2k::J2kError::DimensionOverflow { .. }
        | rust_j2k::J2kError::RateTargetUnreachable { .. } => Error::Argument,
        _ => Error::InvalidOperation,
    }
}

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

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

#[cfg(all(test, feature = "jpeg2000"))]
mod tests {
    use super::*;
    use std::io::Cursor;

    fn rgba(width: i32, height: i32) -> ManagedImage {
        let mut image = ManagedImage::new(
            width,
            height,
            ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA,
        )
        .unwrap();
        for pixel in 0..image.red.len() {
            image.red[pixel] = u8::try_from((pixel * 17) & 255).unwrap();
            image.green[pixel] = u8::try_from((pixel * 29 + 3) & 255).unwrap();
            image.blue[pixel] = u8::try_from((pixel * 43 + 7) & 255).unwrap();
            image.alpha[pixel] = u8::try_from((pixel * 11 + 101) & 255).unwrap();
        }
        image
    }

    #[test]
    fn lossless_codestream_and_jp2_round_trip_all_channels() {
        let source = rgba(17, 9);
        for format in [J2kFormat::Codestream, J2kFormat::Jp2] {
            let encoded =
                J2kCodec::encode(&source, J2kEncodeOptions::default().with_format(format))
                    .expect("encode");
            let decoded =
                J2kCodec::decode_bytes(&encoded, J2kDecodeOptions::default()).expect("decode");
            assert_eq!(decoded, source);
        }
    }

    #[test]
    fn alpha_only_substitution_matches_managed_image_creator() {
        let mut source = ManagedImage::new(2, 1, ManagedImageImageChannels::ALPHA).unwrap();
        source.alpha.copy_from_slice(&[17, 231]);
        let encoded = J2kCodec::encode(&source, J2kEncodeOptions::default()).unwrap();
        let decoded = J2kCodec::decode_bytes(&encoded, J2kDecodeOptions::default()).unwrap();
        assert_eq!(decoded.red, [17, 231]);
        assert_eq!(decoded.green, [17, 231]);
        assert_eq!(decoded.blue, [17, 231]);
        assert_eq!(decoded.alpha, [255, 255]);
    }

    #[test]
    fn discard_levels_reduce_dimensions_and_stream_boundary_is_bounded() {
        let source = rgba(64, 32);
        let encoded = J2kCodec::encode(&source, J2kEncodeOptions::default()).unwrap();
        let reduced =
            J2kCodec::decode_bytes(&encoded, J2kDecodeOptions::default().with_discard_levels(1))
                .unwrap();
        assert_eq!((reduced.width, reduced.height), (32, 16));

        let codec = J2kCodec::new(J2kDecodeOptions::default().with_limits(8, 64));
        assert_eq!(
            codec.decode(Box::new(Cursor::new(encoded))),
            Err(Error::Argument)
        );

        let encoded = J2kCodec::encode(&source, J2kEncodeOptions::default()).unwrap();
        assert_eq!(
            J2kCodec::decode_bytes(
                &encoded,
                J2kDecodeOptions::default().with_limits(encoded.len(), 128),
            ),
            Err(Error::Argument)
        );
    }

    #[test]
    fn invalid_data_and_lossy_settings_fail_without_panicking() {
        assert!(matches!(
            J2kCodec::decode_bytes(b"not jpeg2000", J2kDecodeOptions::default()),
            Err(Error::Parse { .. })
        ));
        let source = rgba(2, 2);
        assert_eq!(
            J2kCodec::encode(
                &source,
                J2kEncodeOptions::default().with_compression(J2kCompression::Lossy {
                    compression_ratio: f32::NAN,
                }),
            ),
            Err(Error::Argument)
        );
        assert!(matches!(
            J2kCodec::decode_bytes(
                b"\0\0\0\x0cjP  \r\n\x87\n\0\0\0",
                J2kDecodeOptions::default(),
            ),
            Err(Error::Parse { .. })
        ));
        assert_eq!(
            J2kCodec::encode(
                &source,
                J2kEncodeOptions::default().with_max_encoded_bytes(32),
            ),
            Err(Error::Argument)
        );
    }

    #[test]
    fn lossy_mode_preserves_layout_with_bounded_sample_error() {
        let source = rgba(64, 64);
        let encoded = J2kCodec::encode(
            &source,
            J2kEncodeOptions::default()
                .with_format(J2kFormat::Jp2)
                .with_compression(J2kCompression::Lossy {
                    compression_ratio: 8.0,
                }),
        )
        .expect("lossy encode");
        let decoded =
            J2kCodec::decode_bytes(&encoded, J2kDecodeOptions::default().with_quality_layers(1))
                .expect("lossy decode");
        assert_eq!(
            (decoded.width, decoded.height, decoded.channels),
            (source.width, source.height, source.channels)
        );

        let total_error: u64 = source
            .red
            .iter()
            .chain(&source.green)
            .chain(&source.blue)
            .chain(&source.alpha)
            .zip(
                decoded
                    .red
                    .iter()
                    .chain(&decoded.green)
                    .chain(&decoded.blue)
                    .chain(&decoded.alpha),
            )
            .map(|(expected, actual)| u64::from(expected.abs_diff(*actual)))
            .sum();
        let samples = u64::try_from(source.red.len() * 4).unwrap();
        assert!(
            total_error > 0,
            "lossy mode unexpectedly reconstructed exactly"
        );
        assert!(total_error / samples < 32, "mean sample error is too high");
    }

    #[test]
    fn openjpeg_2_5_4_golden_retains_sixteen_bit_samples() {
        // Generated by OpenJPEG 2.5.4 `opj_compress` from the deterministic
        // 64x64 unsigned 16-bit gradient asserted below.
        const GOLDEN_HEX: &str = "ff4fff5100290000000000400000004000000000000000000000004000000040000000000000000000010f0101ff52000c00000001000504040001ff5c00134080888890888890888890888890888890ff640025000143726561746564206279204f70656e4a5045472076657273696f6e20322e352e34ff90000a00000000011c0001ff93dff890500c58e2753ee5f7dd26b3c7fe1811000fa8120007ce080d020629b05f7dbf0c199c5f0c44c3ff0306c001f3858000f90180221a085d6dcb5f4686076b4a7f00d09a4b7f01627fc1ff4005e000f9c440007c80c036a199ae63a3628986621949a89ff8bdd889db3a01813f439e44760735fc3f3e871dc0ff0278001f20f80007c2385fa7c788a00db0790644473e98d48fd92b5faff3c71e10932fde0e8f81be8b5bafccacfdd7cf9f9eab0ab177092bc3e079cc1037c0679d9aecfa3be703c07f2670001f09700007438b6201aba8dfc3791d615480235541001a680dce59014e5d9a88d45e9373f9519239613060c182ef7f7816874a3e947ceeaf7fe19cdb5b3899ff0636ff7fe18c4fffd9";
        let encoded = GOLDEN_HEX
            .as_bytes()
            .chunks_exact(2)
            .map(|pair| {
                let pair = std::str::from_utf8(pair).unwrap();
                u8::from_str_radix(pair, 16).unwrap()
            })
            .collect::<Vec<_>>();
        let decoded = J2kCodec::decode_interleaved(&encoded, J2kDecodeOptions::default())
            .expect("decode OpenJPEG golden");
        assert_eq!((decoded.width(), decoded.height()), (64, 64));
        assert_eq!(decoded.number_of_components(), 1);
        let component = &decoded.components()[0];
        assert_eq!((component.precision(), component.is_signed()), (16, false));
        let expected = (0..64)
            .flat_map(|y| (0..64).map(move |x| (x * 65_535 / 64) ^ ((y * 3) & 0xffff)))
            .collect::<Vec<_>>();
        assert_eq!(component.samples(), expected);
    }
}

#[cfg(all(test, feature = "rust-j2k"))]
mod rust_tests {
    use super::*;
    use std::io::Cursor;

    fn rgba(width: i32, height: i32) -> ManagedImage {
        let mut image = ManagedImage::new(
            width,
            height,
            ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA,
        )
        .unwrap();
        for pixel in 0..image.red.len() {
            image.red[pixel] = u8::try_from((pixel * 17) & 255).unwrap();
            image.green[pixel] = u8::try_from((pixel * 29 + 3) & 255).unwrap();
            image.blue[pixel] = u8::try_from((pixel * 43 + 7) & 255).unwrap();
            image.alpha[pixel] = u8::try_from((pixel * 11 + 101) & 255).unwrap();
        }
        image
    }

    #[test]
    fn lossless_codestream_and_jp2_round_trip_all_channels() {
        let source = rgba(17, 9);
        for format in [J2kFormat::Codestream, J2kFormat::Jp2] {
            let encoded =
                RustJ2kCodec::encode(&source, J2kEncodeOptions::default().with_format(format))
                    .expect("encode");
            let decoded =
                RustJ2kCodec::decode_bytes(&encoded, J2kDecodeOptions::default()).expect("decode");
            assert_eq!(decoded, source);
        }
    }

    #[test]
    fn alpha_only_substitution_matches_core_j2k() {
        let mut source = ManagedImage::new(2, 1, ManagedImageImageChannels::ALPHA).unwrap();
        source.alpha.copy_from_slice(&[17, 231]);
        let encoded = RustJ2kCodec::encode(&source, J2kEncodeOptions::default()).unwrap();
        let decoded = RustJ2kCodec::decode_bytes(&encoded, J2kDecodeOptions::default()).unwrap();
        assert_eq!(decoded.red, [17, 231]);
        assert_eq!(decoded.green, [17, 231]);
        assert_eq!(decoded.blue, [17, 231]);
        assert_eq!(decoded.alpha, [255, 255]);
    }

    #[test]
    fn reduced_resolution_and_stream_limits_are_enforced() {
        let source = rgba(64, 32);
        let encoded = RustJ2kCodec::encode(&source, J2kEncodeOptions::default()).unwrap();
        let reduced = RustJ2kCodec::decode_bytes(
            &encoded,
            J2kDecodeOptions::default().with_discard_levels(1),
        )
        .unwrap();
        assert_eq!((reduced.width, reduced.height), (32, 16));

        let codec = RustJ2kCodec::new(J2kDecodeOptions::default().with_limits(8, 64));
        assert_eq!(
            codec.decode(Box::new(Cursor::new(encoded))),
            Err(Error::Argument)
        );
    }

    #[test]
    fn malformed_limits_and_output_caps_fail_without_panicking() {
        assert!(matches!(
            RustJ2kCodec::decode_bytes(b"not jpeg2000", J2kDecodeOptions::default()),
            Err(Error::Parse { .. })
        ));
        let source = rgba(2, 2);
        assert_eq!(
            RustJ2kCodec::encode(
                &source,
                J2kEncodeOptions::default().with_compression(J2kCompression::Lossy {
                    compression_ratio: f32::NAN,
                }),
            ),
            Err(Error::Argument)
        );
        assert_eq!(
            RustJ2kCodec::encode(
                &source,
                J2kEncodeOptions::default().with_max_encoded_bytes(32),
            ),
            Err(Error::Argument)
        );
    }

    #[test]
    fn lossy_ratios_preserve_layout_and_reduce_size() {
        let source = rgba(128, 128);
        let low = RustJ2kCodec::encode(
            &source,
            J2kEncodeOptions::default().with_compression(J2kCompression::Lossy {
                compression_ratio: 4.0,
            }),
        )
        .unwrap();
        let high = RustJ2kCodec::encode(
            &source,
            J2kEncodeOptions::default().with_compression(J2kCompression::Lossy {
                compression_ratio: 12.0,
            }),
        )
        .unwrap();
        assert!(high.len() < low.len());
        let decoded =
            RustJ2kCodec::decode_bytes(&high, J2kDecodeOptions::default().with_quality_layers(1))
                .unwrap();
        assert_eq!(
            (decoded.width, decoded.height, decoded.channels),
            (source.width, source.height, source.channels)
        );
        assert_ne!(decoded, source);
    }

    #[test]
    fn progressive_quality_layers_are_applied_in_order() {
        let source = rgba(128, 128);
        let encoded = RustJ2kCodec::encode(
            &source,
            J2kEncodeOptions::default().with_compression(J2kCompression::Lossy {
                compression_ratio: 8.0,
            }),
        )
        .unwrap();
        let first = RustJ2kCodec::decode_bytes(
            &encoded,
            J2kDecodeOptions::default().with_quality_layers(1),
        )
        .unwrap();
        let second = RustJ2kCodec::decode_bytes(
            &encoded,
            J2kDecodeOptions::default().with_quality_layers(2),
        )
        .unwrap();
        let complete = RustJ2kCodec::decode_bytes(&encoded, J2kDecodeOptions::default()).unwrap();

        let error = |decoded: &ManagedImage| -> u64 {
            source
                .red
                .iter()
                .chain(&source.green)
                .chain(&source.blue)
                .chain(&source.alpha)
                .zip(
                    decoded
                        .red
                        .iter()
                        .chain(&decoded.green)
                        .chain(&decoded.blue)
                        .chain(&decoded.alpha),
                )
                .map(|(expected, actual)| u64::from(expected.abs_diff(*actual)))
                .sum()
        };
        assert!(error(&first) >= error(&second));
        assert!(error(&second) >= error(&complete));
        assert_ne!(first, complete);
    }

    #[test]
    fn strict_rejects_missing_eoc_and_permissive_repairs_it() {
        let source = rgba(16, 16);
        for format in [J2kFormat::Codestream, J2kFormat::Jp2] {
            let mut encoded =
                RustJ2kCodec::encode(&source, J2kEncodeOptions::default().with_format(format))
                    .unwrap();
            assert!(encoded.ends_with(&[0xff, 0xd9]));
            encoded.truncate(encoded.len() - 2);
            assert!(RustJ2kCodec::decode_bytes(&encoded, J2kDecodeOptions::default()).is_err());
            assert_eq!(
                RustJ2kCodec::decode_bytes(
                    &encoded,
                    J2kDecodeOptions::default().with_strict_mode(false),
                )
                .unwrap(),
                source
            );
        }
    }

    #[test]
    fn native_planes_preserve_mixed_precision_signedness_and_five_component_order() {
        let width = 8_u32;
        let height = 4_u32;
        let pixels = usize::try_from(width * height).unwrap();
        let specifications = [
            (8_u8, false),
            (12_u8, true),
            (10_u8, false),
            (8_u8, false),
            (16_u8, true),
        ];
        let mut plane_bytes = Vec::new();
        let mut expected = Vec::new();
        for (component, (precision, signed)) in specifications.iter().copied().enumerate() {
            let mut bytes = Vec::new();
            let mut values = Vec::new();
            for pixel in 0..pixels {
                let value = if signed {
                    i32::try_from(pixel).unwrap() - 15 - i32::try_from(component).unwrap()
                } else {
                    i32::try_from(pixel * (component + 1)).unwrap()
                };
                values.push(value);
                let width = usize::from(precision).div_ceil(8);
                bytes.extend_from_slice(&value.to_le_bytes()[..width]);
            }
            plane_bytes.push(bytes);
            expected.push(values);
        }
        let planes = plane_bytes
            .iter()
            .zip(specifications)
            .map(
                |(data, (bit_depth, signed))| rust_j2k::J2kLosslessTypedComponentPlane {
                    data,
                    x_rsiz: 1,
                    y_rsiz: 1,
                    bit_depth,
                    signed,
                },
            )
            .collect::<Vec<_>>();
        let samples =
            rust_j2k::J2kLosslessTypedComponentSamples::new(&planes, width, height).unwrap();
        let encoded = rust_j2k::encode_j2k_lossless_typed_components(
            samples,
            &rust_j2k::J2kLosslessEncodeOptions::default()
                .with_cpu_only_backend()
                .with_reversible_transform(rust_j2k::ReversibleTransform::None53),
        )
        .unwrap();
        let decoded =
            RustJ2kCodec::decode_interleaved(&encoded.codestream, J2kDecodeOptions::default())
                .unwrap();
        assert_eq!(decoded.number_of_components(), 5);
        for (index, component) in decoded.components().iter().enumerate() {
            assert_eq!(component.precision(), specifications[index].0);
            assert_eq!(component.is_signed(), specifications[index].1);
            assert_eq!(component.samples(), expected[index]);
        }
    }

    #[test]
    fn independent_gray_and_rgb_codestreams_decode_without_rgba_assumptions() {
        for components in [1_u16, 3] {
            let width = 11_u32;
            let height = 7_u32;
            let samples = (0..usize::try_from(width * height).unwrap() * usize::from(components))
                .map(|index| u8::try_from((index * 23 + 7) & 255).unwrap())
                .collect::<Vec<_>>();
            let input =
                rust_j2k::J2kLosslessSamples::new(&samples, width, height, components, 8, false)
                    .unwrap();
            let encoded = rust_j2k::encode_j2k_lossless(
                input,
                &rust_j2k::J2kLosslessEncodeOptions::default().with_cpu_only_backend(),
            )
            .unwrap();
            let decoded =
                RustJ2kCodec::decode_interleaved(&encoded.codestream, J2kDecodeOptions::default())
                    .unwrap();
            assert_eq!(decoded.number_of_components(), usize::from(components));
            let managed = interleaved_to_managed(&decoded).unwrap();
            assert_eq!((managed.width, managed.height), (11, 7));
        }
    }

    #[test]
    fn unsupported_subsampling_and_excess_components_are_rejected() {
        let width = 8_u32;
        let height = 8_u32;
        let full = vec![37_u8; usize::try_from(width * height).unwrap()];
        let half_width = vec![91_u8; usize::try_from(width / 2 * height).unwrap()];
        let planes = [
            rust_j2k::J2kLosslessTypedComponentPlane {
                data: &full,
                x_rsiz: 1,
                y_rsiz: 1,
                bit_depth: 8,
                signed: false,
            },
            rust_j2k::J2kLosslessTypedComponentPlane {
                data: &half_width,
                x_rsiz: 2,
                y_rsiz: 1,
                bit_depth: 8,
                signed: false,
            },
        ];
        let samples =
            rust_j2k::J2kLosslessTypedComponentSamples::new(&planes, width, height).unwrap();
        let subsampled = rust_j2k::encode_j2k_lossless_typed_components(
            samples,
            &rust_j2k::J2kLosslessEncodeOptions::default()
                .with_cpu_only_backend()
                .with_reversible_transform(rust_j2k::ReversibleTransform::None53),
        )
        .unwrap();
        assert!(matches!(
            RustJ2kCodec::decode_interleaved(&subsampled.codestream, J2kDecodeOptions::default()),
            Err(Error::Argument)
        ));

        let six_components = vec![19_u8; usize::try_from(width * height).unwrap() * 6];
        let samples =
            rust_j2k::J2kLosslessSamples::new(&six_components, width, height, 6, 8, false).unwrap();
        let encoded = rust_j2k::encode_j2k_lossless(
            samples,
            &rust_j2k::J2kLosslessEncodeOptions::default().with_cpu_only_backend(),
        )
        .unwrap();
        assert_eq!(
            RustJ2kCodec::decode_interleaved(&encoded.codestream, J2kDecodeOptions::default()),
            Err(Error::Argument)
        );
    }

    #[cfg(feature = "jpeg2000")]
    #[test]
    fn openjpeg_and_rust_backends_interoperate_both_directions() {
        let source = rgba(32, 24);
        let rust = RustJ2kCodec::encode(&source, J2kEncodeOptions::default()).unwrap();
        assert_eq!(
            J2kCodec::decode_bytes(&rust, J2kDecodeOptions::default()).unwrap(),
            source
        );
        let native = J2kCodec::encode(&source, J2kEncodeOptions::default()).unwrap();
        assert_eq!(
            RustJ2kCodec::decode_bytes(&native, J2kDecodeOptions::default()).unwrap(),
            source
        );
    }
}
