// Copyright 2018-2025 the Deno authors. MIT license.

use std::cell::RefCell;
use std::rc::Rc;
use std::time::Duration;

use deno_core::futures::channel::oneshot;
use deno_core::op2;
use deno_core::v8;
use deno_core::webidl::WebIdlInterfaceConverter;
use deno_core::GarbageCollected;
use deno_core::WebIDL;
use deno_error::JsErrorBox;
use wgpu_core::device::HostMap as MapMode;

use crate::Instance;

#[derive(WebIDL)]
#[webidl(dictionary)]
pub(crate) struct GPUBufferDescriptor {
    #[webidl(default = String::new())]
    pub label: String,

    pub size: u64,
    #[options(enforce_range = true)]
    pub usage: u32,
    #[webidl(default = false)]
    pub mapped_at_creation: bool,
}

#[derive(Debug, thiserror::Error, deno_error::JsError)]
pub enum BufferError {
    #[class(generic)]
    #[error(transparent)]
    Canceled(#[from] oneshot::Canceled),
    #[class("DOMExceptionOperationError")]
    #[error(transparent)]
    Access(#[from] wgpu_core::resource::BufferAccessError),
    #[class("DOMExceptionOperationError")]
    #[error("{0}")]
    Operation(&'static str),
    #[class(inherit)]
    #[error(transparent)]
    Other(#[from] JsErrorBox),
}

pub struct GPUBuffer {
    pub instance: Instance,
    pub error_handler: super::error::ErrorHandler,

    pub id: wgpu_core::id::BufferId,
    pub device: wgpu_core::id::DeviceId,

    pub label: String,

    pub size: u64,
    pub usage: u32,

    pub map_state: RefCell<&'static str>,
    pub map_mode: RefCell<Option<MapMode>>,

    pub mapped_js_buffers: RefCell<Vec<v8::Global<v8::ArrayBuffer>>>,
}

impl Drop for GPUBuffer {
    fn drop(&mut self) {
        self.instance.buffer_drop(self.id);
    }
}

impl WebIdlInterfaceConverter for GPUBuffer {
    const NAME: &'static str = "GPUBuffer";
}

impl GarbageCollected for GPUBuffer {}

#[op2]
impl GPUBuffer {
    #[getter]
    #[string]
    fn label(&self) -> String {
        self.label.clone()
    }
    #[setter]
    #[string]
    fn label(&self, #[webidl] _label: String) {
        // TODO(@crowlKats): no-op, needs wpgu to implement changing the label
    }

    #[getter]
    #[number]
    fn size(&self) -> u64 {
        self.size
    }
    #[getter]
    fn usage(&self) -> u32 {
        self.usage
    }

    #[getter]
    #[string]
    fn map_state(&self) -> &'static str {
        *self.map_state.borrow()
    }

    #[async_method]
    async fn map_async(
        &self,
        #[webidl(options(enforce_range = true))] mode: u32,
        #[webidl(default = 0)] offset: u64,
        #[webidl] size: Option<u64>,
    ) -> Result<(), BufferError> {
        let read_mode = (mode & 0x0001) == 0x0001;
        let write_mode = (mode & 0x0002) == 0x0002;
        if (read_mode && write_mode) || (!read_mode && !write_mode) {
            return Err(BufferError::Operation(
                "exactly one of READ or WRITE map mode must be set",
            ));
        }

        let mode = if read_mode {
            MapMode::Read
        } else {
            assert!(write_mode);
            MapMode::Write
        };

        {
            *self.map_state.borrow_mut() = "pending";
        }

        let (sender, receiver) = oneshot::channel::<wgpu_core::resource::BufferAccessResult>();

        {
            let callback = Box::new(move |status| {
                sender.send(status).unwrap();
            });

            let err = self
                .instance
                .buffer_map_async(
                    self.id,
                    offset,
                    size,
                    wgpu_core::resource::BufferMapOperation {
                        host: mode,
                        callback: Some(callback),
                    },
                )
                .err();

            if err.is_some() {
                self.error_handler.push_error(err);
                return Err(BufferError::Operation("validation error occurred"));
            }
        }

        let done = Rc::new(RefCell::new(false));
        let done_ = done.clone();
        let device_poll_fut = async move {
            while !*done.borrow() {
                {
                    self.instance
                        .device_poll(self.device, wgpu_types::PollType::wait())
                        .unwrap();
                }
                tokio::time::sleep(Duration::from_millis(10)).await;
            }
            Ok::<(), BufferError>(())
        };

        let receiver_fut = async move {
            receiver.await??;
            let mut done = done_.borrow_mut();
            *done = true;
            Ok::<(), BufferError>(())
        };

        tokio::try_join!(device_poll_fut, receiver_fut)?;

        *self.map_state.borrow_mut() = "mapped";
        *self.map_mode.borrow_mut() = Some(mode);

        Ok(())
    }

    fn get_mapped_range<'s>(
        &self,
        scope: &mut v8::HandleScope<'s>,
        #[webidl(default = 0)] offset: u64,
        #[webidl] size: Option<u64>,
    ) -> Result<v8::Local<'s, v8::ArrayBuffer>, BufferError> {
        let (slice_pointer, range_size) = self
            .instance
            .buffer_get_mapped_range(self.id, offset, size)
            .map_err(BufferError::Access)?;

        let mode = self.map_mode.borrow();
        let mode = mode.as_ref().unwrap();

        let bs = if mode == &MapMode::Write {
            unsafe extern "C" fn noop_deleter_callback(
                _data: *mut std::ffi::c_void,
                _byte_length: usize,
                _deleter_data: *mut std::ffi::c_void,
            ) {
            }

            // SAFETY: creating a backing store from the pointer and length provided by wgpu
            unsafe {
                v8::ArrayBuffer::new_backing_store_from_ptr(
                    slice_pointer.as_ptr() as _,
                    range_size as usize,
                    noop_deleter_callback,
                    std::ptr::null_mut(),
                )
            }
        } else {
            // SAFETY: creating a vector from the pointer and length provided by wgpu
            let slice =
                unsafe { std::slice::from_raw_parts(slice_pointer.as_ptr(), range_size as usize) };
            v8::ArrayBuffer::new_backing_store_from_vec(slice.to_vec())
        };

        let shared_bs = bs.make_shared();
        let ab = v8::ArrayBuffer::with_backing_store(scope, &shared_bs);

        if mode == &MapMode::Write {
            self.mapped_js_buffers
                .borrow_mut()
                .push(v8::Global::new(scope, ab));
        }

        Ok(ab)
    }

    #[nofast]
    fn unmap(&self, scope: &mut v8::HandleScope) -> Result<(), BufferError> {
        for ab in self.mapped_js_buffers.replace(vec![]) {
            let ab = ab.open(scope);
            ab.detach(None);
        }

        self.instance
            .buffer_unmap(self.id)
            .map_err(BufferError::Access)?;

        *self.map_state.borrow_mut() = "unmapped";

        Ok(())
    }

    #[fast]
    fn destroy(&self) {
        self.instance.buffer_destroy(self.id);
    }
}
