use core::iter;
use core::mem::size_of;
use egui_wgpu::wgpu;
use egui_wgpu::wgpu::{Device, Extent3d, Queue, Texture};
use image::RgbaImage;
use std::sync::mpsc::channel;

use crate::wgpu::WAIT_TIMEOUT;

pub(crate) fn texture_to_image(device: &Device, queue: &Queue, texture: &Texture) -> RgbaImage {
    let buffer_dimensions =
        BufferDimensions::new(texture.width() as usize, texture.height() as usize);

    let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
        label: Some("Texture to bytes output buffer"),
        size: (buffer_dimensions.padded_bytes_per_row * buffer_dimensions.height) as u64,
        usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
        mapped_at_creation: false,
    });

    let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
        label: Some("Texture to bytes encoder"),
    });

    // Copy the data from the texture to the buffer
    encoder.copy_texture_to_buffer(
        texture.as_image_copy(),
        wgpu::TexelCopyBufferInfo {
            buffer: &output_buffer,
            layout: wgpu::TexelCopyBufferLayout {
                offset: 0,
                bytes_per_row: Some(buffer_dimensions.padded_bytes_per_row as u32),
                rows_per_image: None,
            },
        },
        Extent3d {
            width: texture.width(),
            height: texture.height(),
            depth_or_array_layers: 1,
        },
    );

    let submission_index = queue.submit(iter::once(encoder.finish()));

    // Note that we're not calling `.await` here.
    let buffer_slice = output_buffer.slice(..);
    // Sets the buffer up for mapping, sending over the result of the mapping back to us when it is finished.
    let (sender, receiver) = channel();
    buffer_slice.map_async(wgpu::MapMode::Read, move |v| drop(sender.send(v)));

    // Poll the device in a blocking manner so that our future resolves.
    device
        .poll(wgpu::PollType::Wait {
            submission_index: Some(submission_index),
            timeout: Some(WAIT_TIMEOUT),
        })
        .expect("Failed to poll device");

    receiver.recv().unwrap().unwrap();
    let buffer_slice = output_buffer.slice(..);
    let data = buffer_slice
        .get_mapped_range()
        .expect("Failed to get mapped range");
    let data = data
        .chunks_exact(buffer_dimensions.padded_bytes_per_row)
        .flat_map(|row| row.iter().take(buffer_dimensions.unpadded_bytes_per_row))
        .copied()
        .collect::<Vec<_>>();

    RgbaImage::from_raw(texture.width(), texture.height(), data).expect("Failed to create image")
}

struct BufferDimensions {
    height: usize,
    unpadded_bytes_per_row: usize,
    padded_bytes_per_row: usize,
}

impl BufferDimensions {
    fn new(width: usize, height: usize) -> Self {
        let bytes_per_pixel = size_of::<u32>();
        let unpadded_bytes_per_row = width * bytes_per_pixel;
        let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
        let padded_bytes_per_row_padding = (align - unpadded_bytes_per_row % align) % align;
        let padded_bytes_per_row = unpadded_bytes_per_row + padded_bytes_per_row_padding;
        Self {
            height,
            unpadded_bytes_per_row,
            padded_bytes_per_row,
        }
    }
}
