use crate::definitions::Image;
use image::Pixel;
use proptest::{
    arbitrary::{Arbitrary, any},
    sample::SizeRange,
    strategy::{BoxedStrategy, Strategy},
};
use std::{fmt, ops::Range};

/// Create a strategy to generate arbitrary images with dimensions selected
/// within the specified ranges.
pub(crate) fn arbitrary_image<P>(
    width_range: impl Into<SizeRange>,
    height_range: impl Into<SizeRange>,
) -> BoxedStrategy<Image<P>>
where
    P: Pixel + fmt::Debug,
    P::Subpixel: Arbitrary + fmt::Debug,
    <P::Subpixel as Arbitrary>::Strategy: Clone + 'static,
{
    arbitrary_image_with(any::<P::Subpixel>(), width_range, height_range)
}

/// Create a strategy to generate images with a given subpixel strategy and
/// dimensions selected within the specified ranges.
pub(crate) fn arbitrary_image_with<P>(
    subpixels: impl Strategy<Value = P::Subpixel> + Clone + 'static,
    width_range: impl Into<SizeRange>,
    height_range: impl Into<SizeRange>,
) -> BoxedStrategy<Image<P>>
where
    P: Pixel + fmt::Debug,
    P::Subpixel: fmt::Debug,
{
    dims(width_range, height_range)
        .prop_flat_map(move |(w, h)| fixed_image_with(subpixels.clone(), w, h))
        .boxed()
}

fn fixed_image_with<P>(
    strategy: impl Strategy<Value = P::Subpixel> + 'static,
    width: u32,
    height: u32,
) -> BoxedStrategy<Image<P>>
where
    P: Pixel + fmt::Debug,
    P::Subpixel: fmt::Debug,
{
    let size = (width * height * P::CHANNEL_COUNT as u32) as usize;
    let vecs = proptest::collection::vec(strategy, size);

    vecs.prop_map(move |v| Image::from_vec(width, height, v).unwrap())
        .boxed()
}

fn dims(width: impl Into<SizeRange>, height: impl Into<SizeRange>) -> BoxedStrategy<(u32, u32)> {
    let (width, height) = (width.into(), height.into());
    let width: Range<usize> = width.into();
    let height: Range<usize> = height.into();
    width
        .prop_flat_map(move |w| height.clone().prop_map(move |h| (w as u32, h as u32)))
        .boxed()
}

#[cfg(not(miri))]
#[cfg(test)]
mod proptests {
    use super::*;
    use image::{Luma, Rgb};
    use proptest::prelude::*;

    proptest! {
        #[test]
        fn test_arbitrary_fixed_rgb(img in arbitrary_image::<Rgb<u8>>(3, 7)) {
            prop_assert_eq!(img.width(), 3);
            prop_assert_eq!(img.height(), 7);
        }

        #[test]
        fn test_arbitrary_gray(img in arbitrary_image::<Luma<u8>>(1..30, 2..=150)) {
            prop_assert!((1..30).contains(&img.width()));
            prop_assert!((2..=150).contains(&img.height()));
        }
    }
}
