#![cfg(all(target_arch = "wasm32", not(target_os = "emscripten")))]

use wasm_bindgen::JsCast;
use wgpu::ExternalImageSource;
use wgpu_test::{fail_if, gpu_test, GpuTestConfiguration};

#[gpu_test]
static IMAGE_BITMAP_IMPORT: GpuTestConfiguration =
    GpuTestConfiguration::new().run_async(|ctx| async move {
        let image_encoded = include_bytes!("3x3_colors.png");

        // Create an array-of-arrays for Blob's constructor
        let array = js_sys::Array::new();
        array.push(&js_sys::Uint8Array::from(&image_encoded[..]));

        // We're passing an array of Uint8Arrays
        let blob = web_sys::Blob::new_with_u8_array_sequence(&array).unwrap();

        // Parse the image from the blob

        // Because we need to call the function in a way that isn't bound by
        // web_sys, we need to manually construct the options struct and call
        // the function.
        let image_bitmap_function: js_sys::Function = web_sys::window()
            .unwrap()
            .get("createImageBitmap")
            .unwrap()
            .dyn_into()
            .unwrap();

        let options_arg = js_sys::Object::new();
        js_sys::Reflect::set(
            &options_arg,
            &wasm_bindgen::JsValue::from_str("premultiplyAlpha"),
            &wasm_bindgen::JsValue::from_str("none"),
        )
        .unwrap();
        let image_bitmap_promise: js_sys::Promise = image_bitmap_function
            .call2(&wasm_bindgen::JsValue::UNDEFINED, &blob, &options_arg)
            .unwrap()
            .dyn_into()
            .unwrap();

        // Wait for the parsing to be done
        let image_bitmap: web_sys::ImageBitmap =
            wasm_bindgen_futures::JsFuture::from(image_bitmap_promise)
                .await
                .unwrap()
                .dyn_into()
                .unwrap();

        // Sanity checks
        assert_eq!(image_bitmap.width(), 3);
        assert_eq!(image_bitmap.height(), 3);

        // Due to restrictions with premultiplication with ImageBitmaps, we also create an HtmlCanvasElement
        // by drawing the image bitmap onto the canvas.
        let canvas: web_sys::HtmlCanvasElement = web_sys::window()
            .unwrap()
            .document()
            .unwrap()
            .create_element("canvas")
            .unwrap()
            .dyn_into()
            .unwrap();
        canvas.set_width(3);
        canvas.set_height(3);

        let d2_context: web_sys::CanvasRenderingContext2d = canvas
            .get_context("2d")
            .unwrap()
            .unwrap()
            .dyn_into()
            .unwrap();
        d2_context
            .draw_image_with_image_bitmap(&image_bitmap, 0.0, 0.0)
            .unwrap();

        // Decode it cpu side
        let raw_image = image::load_from_memory_with_format(image_encoded, image::ImageFormat::Png)
            .unwrap()
            .into_rgba8();

        // Set of test cases to test with image import
        #[derive(Debug, Copy, Clone)]
        enum TestCase {
            // Import the image as normal
            Normal,
            // Sets the FlipY flag. Deals with global state on GLES, so run before other tests to ensure it's reset.
            //
            // Only works on canvases.
            FlipY,
            // Sets the premultiplied alpha flag. Deals with global state on GLES, so run before other tests to ensure it's reset.
            //
            // Only works on canvases.
            Premultiplied,
            // Sets the color space to P3.
            //
            // Only works on canvases.
            ColorSpace,
            // Sets the premultiplied alpha flag. Deals with global state on GLES, so run before other tests to ensure it's reset.
            // Set both the input offset and output offset to 1 in x, so the first column is omitted.
            TrimLeft,
            // Set the size to 2 in x, so the last column is omitted
            TrimRight,
            // Set only the output offset to 1, so the second column gets the first column's data.
            SlideRight,
            // Try to copy from out of bounds of the source image
            SourceOutOfBounds,
            // Try to copy from out of bounds of the destination image
            DestOutOfBounds,
            // Try to copy more than one slice from the source
            MultiSliceCopy,
            // Copy into the second slice of a 2D array texture,
            SecondSliceCopy,
        }
        let sources = [
            ExternalImageSource::ImageBitmap(image_bitmap),
            ExternalImageSource::HTMLCanvasElement(canvas),
        ];
        let cases = [
            TestCase::Normal,
            TestCase::FlipY,
            TestCase::Premultiplied,
            TestCase::ColorSpace,
            TestCase::TrimLeft,
            TestCase::TrimRight,
            TestCase::SlideRight,
            TestCase::SourceOutOfBounds,
            TestCase::DestOutOfBounds,
            TestCase::MultiSliceCopy,
            TestCase::SecondSliceCopy,
        ];

        for source in sources {
            for case in cases {
                // Copy the data, so we can modify it for tests
                let mut raw_image = raw_image.clone();
                // The origin used for the external copy on the source side.
                let mut src_origin = wgpu::Origin2d::ZERO;
                // If the source should be flipped in Y
                let mut src_flip_y = false;
                // The origin used for the external copy on the destination side.
                let mut dest_origin = wgpu::Origin3d::ZERO;
                // The layer the external image's data should end up in.
                let mut dest_data_layer = 0;
                // Color space the destination is in.
                let mut dest_color_space = wgpu::PredefinedColorSpace::Srgb;
                // If the destination image is premultiplied.
                let mut dest_premultiplied = false;
                // Size of the external copy
                let mut copy_size = wgpu::Extent3d {
                    width: 3,
                    height: 3,
                    depth_or_array_layers: 1,
                };
                // Width of the destination texture
                let mut dest_width = 3;
                // Layer count of the destination texture
                let mut dest_layers = 1;

                // If the test is supposed to be valid call to copyExternal.
                let mut valid = true;
                match case {
                    TestCase::Normal => {}
                    TestCase::FlipY => {
                        valid = !matches!(source, wgpu::ExternalImageSource::ImageBitmap(_));
                        src_flip_y = true;
                        for x in 0..3 {
                            let top = raw_image[(x, 0)];
                            let bottom = raw_image[(x, 2)];
                            raw_image[(x, 0)] = bottom;
                            raw_image[(x, 2)] = top;
                        }
                    }
                    TestCase::Premultiplied => {
                        valid = !matches!(source, wgpu::ExternalImageSource::ImageBitmap(_));
                        dest_premultiplied = true;
                        for pixel in raw_image.pixels_mut() {
                            let mut float_pix = pixel.0.map(|v| v as f32 / 255.0);
                            float_pix[0] *= float_pix[3];
                            float_pix[1] *= float_pix[3];
                            float_pix[2] *= float_pix[3];
                            pixel.0 = float_pix.map(|v| (v * 255.0).round() as u8);
                        }
                    }
                    TestCase::ColorSpace => {
                        valid = ctx
                            .adapter_downlevel_capabilities
                            .flags
                            .contains(wgpu::DownlevelFlags::UNRESTRICTED_EXTERNAL_TEXTURE_COPIES);
                        dest_color_space = wgpu::PredefinedColorSpace::DisplayP3;

                        // As we don't test, we don't bother converting the color spaces
                        // in the image as that's relatively annoying.
                    }
                    TestCase::TrimLeft => {
                        valid = ctx
                            .adapter_downlevel_capabilities
                            .flags
                            .contains(wgpu::DownlevelFlags::UNRESTRICTED_EXTERNAL_TEXTURE_COPIES);
                        src_origin.x = 1;
                        dest_origin.x = 1;
                        copy_size.width = 2;
                        for y in 0..3 {
                            raw_image[(0, y)].0 = [0; 4];
                        }
                    }
                    TestCase::TrimRight => {
                        copy_size.width = 2;
                        for y in 0..3 {
                            raw_image[(2, y)].0 = [0; 4];
                        }
                    }
                    TestCase::SlideRight => {
                        dest_origin.x = 1;
                        copy_size.width = 2;
                        for x in (1..3).rev() {
                            for y in 0..3 {
                                raw_image[(x, y)].0 = raw_image[(x - 1, y)].0;
                            }
                        }
                        for y in 0..3 {
                            raw_image[(0, y)].0 = [0; 4];
                        }
                    }
                    TestCase::SourceOutOfBounds => {
                        valid = false;
                        // It's now in bounds for the destination
                        dest_width = 4;
                        copy_size.width = 4;
                    }
                    TestCase::DestOutOfBounds => {
                        valid = false;
                        // It's now out bounds for the destination
                        dest_width = 2;
                    }
                    TestCase::MultiSliceCopy => {
                        valid = false;
                        copy_size.depth_or_array_layers = 2;
                        dest_layers = 2;
                    }
                    TestCase::SecondSliceCopy => {
                        dest_origin.z = 1;
                        dest_data_layer = 1;
                        dest_layers = 2;
                    }
                }

                let texture = ctx.device.create_texture(&wgpu::TextureDescriptor {
                    label: Some("import dest"),
                    size: wgpu::Extent3d {
                        width: dest_width,
                        height: 3,
                        depth_or_array_layers: dest_layers,
                    },
                    mip_level_count: 1,
                    sample_count: 1,
                    dimension: wgpu::TextureDimension::D2,
                    format: wgpu::TextureFormat::Rgba8UnormSrgb,
                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT
                        | wgpu::TextureUsages::COPY_DST
                        | wgpu::TextureUsages::COPY_SRC,
                    view_formats: &[],
                });

                fail_if(
                    &ctx.device,
                    !valid,
                    || {
                        ctx.queue.copy_external_image_to_texture(
                            &wgpu::CopyExternalImageSourceInfo {
                                source: source.clone(),
                                origin: src_origin,
                                flip_y: src_flip_y,
                            },
                            wgpu::CopyExternalImageDestInfo {
                                texture: &texture,
                                mip_level: 0,
                                origin: dest_origin,
                                aspect: wgpu::TextureAspect::All,
                                color_space: dest_color_space,
                                premultiplied_alpha: dest_premultiplied,
                            },
                            copy_size,
                        );
                    },
                    None,
                );

                let readback_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor {
                    label: Some("readback buffer"),
                    size: 4 * 64 * 3,
                    usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
                    mapped_at_creation: false,
                });

                let mut encoder = ctx
                    .device
                    .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
                encoder.copy_texture_to_buffer(
                    wgpu::TexelCopyTextureInfo {
                        texture: &texture,
                        mip_level: 0,
                        origin: wgpu::Origin3d {
                            x: 0,
                            y: 0,
                            z: dest_data_layer,
                        },
                        aspect: wgpu::TextureAspect::All,
                    },
                    wgpu::TexelCopyBufferInfo {
                        buffer: &readback_buffer,
                        layout: wgpu::TexelCopyBufferLayout {
                            offset: 0,
                            bytes_per_row: Some(256),
                            rows_per_image: None,
                        },
                    },
                    wgpu::Extent3d {
                        width: dest_width,
                        height: 3,
                        depth_or_array_layers: 1,
                    },
                );

                ctx.queue.submit(Some(encoder.finish()));
                readback_buffer
                    .slice(..)
                    .map_async(wgpu::MapMode::Read, |_| ());
                ctx.async_poll(wgpu::PollType::wait()).await.unwrap();

                let buffer = readback_buffer.slice(..).get_mapped_range();

                // 64 because of 256 byte alignment / 4.
                let gpu_image = image::RgbaImage::from_vec(64, 3, buffer.to_vec()).unwrap();
                let gpu_image_cropped =
                    image::imageops::crop_imm(&gpu_image, 0, 0, 3, 3).to_image();

                if valid {
                    assert_eq!(
                        raw_image, gpu_image_cropped,
                        "Failed on test case {case:?} {source:?}"
                    );
                } else {
                    assert_ne!(
                        raw_image, gpu_image_cropped,
                        "Failed on test case {case:?} {source:?}"
                    );
                }
            }
        }
    });
