#![cfg(feature = "cuda")]
use crate::GPU_CONTEXT;
use cust::memory::{DeviceBuffer, DevicePointer};

use cust::memory::DeviceCopy;

use crate::Numeric;

use core::ffi::c_void;

#[derive(Debug)]
/// Owned wrapper around a CUDA `DeviceBuffer` allocated from the global
/// CUDA memory pool.
///
/// The wrapper ensures memory is returned to the configured `CudaMemoryPool`
/// when dropped. The generic `T` must implement `Numeric` and `DeviceCopy` so
/// it can be safely used across the host/device boundary.
pub struct CustomDeviceBuffer<T: Numeric + DeviceCopy> {
    pub device_buffer: DeviceBuffer<T>,
}

impl<T: Numeric + DeviceCopy> CustomDeviceBuffer<T> {
    /// Return a `DevicePointer<T>` suitable for passing to CUDA kernels or
    /// runtime APIs.
    pub fn as_device_ptr(&self) -> DevicePointer<T> {
        self.device_buffer.as_device_ptr()
    }
}

impl<T: Numeric + DeviceCopy> Drop for CustomDeviceBuffer<T> {
    fn drop(&mut self) {
        let pool = match &GPU_CONTEXT.get().expect("No GPU Context Set").pool {
            Some(p) => p,
            None => panic!("Cuda not initialized or Gpu Pool is not set up"),
        };

        let _ = pool.free(self.as_device_ptr().as_raw());
    }
}

/// Allocate a device buffer of `size` elements from the global CUDA memory
/// pool and return a `CustomDeviceBuffer` that owns the allocation.
///
/// # Panics
/// - If the global `GPU_CONTEXT` is not initialized or the memory pool is
///   not set up.
pub fn get_device_buffer<T: Numeric + DeviceCopy>(size: usize) -> CustomDeviceBuffer<T> {
    let pool = match &GPU_CONTEXT.get().expect("No GPU Context Set").pool {
        Some(p) => p,
        None => panic!("Cuda not initialized or Gpu Pool is not set up"),
    };

    let ptr_size = size.checked_mul(size_of::<T>()).unwrap();

    if ptr_size == 0 {
        panic!("Attempted a zero size pointer or null pointer creation.");
    }

    let pool_allocated_pointer = pool.allocate(ptr_size).unwrap();
    let device_pointer = DevicePointer::from_raw(pool_allocated_pointer);

    let device_buffer = unsafe { DeviceBuffer::from_raw_parts(device_pointer, size) };

    CustomDeviceBuffer { device_buffer }
}

/// Allocate a device buffer and copy the provided `data` slice into device
/// memory, returning a `CustomDeviceBuffer` owning the result.
pub fn get_device_buffer_from_slice<T: Numeric + DeviceCopy>(data: &[T]) -> CustomDeviceBuffer<T> {
    let device_buffer = get_device_buffer::<T>(data.len());
    unsafe {
        cust::sys::cuMemcpyHtoD_v2(
            device_buffer.as_device_ptr().as_raw(),
            data.as_ptr() as *const c_void,
            data.len() * size_of::<T>(),
        )
    };
    device_buffer
}
