use std::{ffi::CString, ptr};

use candle_core::{Device, DeviceLocation};
use ort_sys::{OrtErrorCode, OrtMemType, OrtMemoryInfoDeviceType};

use crate::Error;

#[repr(transparent)]
pub struct MemoryInfo(pub Device);

impl MemoryInfo {
	pub fn new(device_name: impl AsRef<str>, device_id: usize, mem_type: OrtMemType) -> Result<Self, Error> {
		match device_name.as_ref() {
			"Cpu" | "CudaPinned" => Ok(Self(Device::Cpu)),
			"Cuda" => match mem_type {
				OrtMemType::OrtMemTypeCPUInput | OrtMemType::OrtMemTypeCPUOutput => Ok(Self(Device::Cpu)),
				OrtMemType::OrtMemTypeDefault => Device::new_cuda(device_id)
					.map(Self)
					.map_err(|e| Error::new(OrtErrorCode::ORT_ENGINE_ERROR, e.to_string()))
			},
			"Metal" => Device::new_metal(device_id)
				.map(Self)
				.map_err(|e| Error::new(OrtErrorCode::ORT_ENGINE_ERROR, e.to_string())),
			device_name => Err(Error::new(OrtErrorCode::ORT_NOT_IMPLEMENTED, format!("ort-candle does not support the '{device_name}' device")))
		}
	}

	pub fn device(&self) -> &Device {
		&self.0
	}

	pub fn device_type(&self) -> OrtMemoryInfoDeviceType {
		match &self.0 {
			Device::Cpu => OrtMemoryInfoDeviceType::OrtMemoryInfoDeviceType_CPU,
			Device::Cuda(_) | Device::Metal(_) => OrtMemoryInfoDeviceType::OrtMemoryInfoDeviceType_GPU
		}
	}

	pub fn device_name(&self) -> &'static str {
		let sys_str = self.device_name_sys();
		&sys_str[..sys_str.len() - 1]
	}

	pub fn device_name_sys(&self) -> &'static str {
		match &self.0 {
			Device::Cpu => "Cpu\0",
			Device::Cuda(_) => "Cuda\0",
			Device::Metal(_) => "Metal\0"
		}
	}

	pub fn device_id(&self) -> usize {
		match self.0.location() {
			DeviceLocation::Cpu => 0,
			DeviceLocation::Cuda { gpu_id } => gpu_id,
			DeviceLocation::Metal { gpu_id } => gpu_id
		}
	}

	pub fn memory_type(&self) -> OrtMemType {
		OrtMemType::OrtMemTypeDefault
	}
}

impl PartialEq for MemoryInfo {
	fn eq(&self, other: &Self) -> bool {
		self.0.same_device(&other.0)
	}
}

#[repr(C)]
pub struct Allocator<'m> {
	_sys_api: ort_sys::OrtAllocator,
	pub memory_info: &'m MemoryInfo
}

impl<'m> Allocator<'m> {
	pub const fn new(memory_info: &'m MemoryInfo) -> Self {
		Self {
			_sys_api: ort_sys::OrtAllocator {
				version: ort_sys::ORT_API_VERSION,
				Alloc: Some(sys_allocator_alloc),
				Free: Some(sys_allocator_free),
				Info: Some(sys_allocator_info),
				Reserve: Some(sys_allocator_reserve)
			},
			memory_info
		}
	}
}

pub static DEFAULT_CPU_ALLOCATOR: Allocator = Allocator::new(&MemoryInfo(Device::Cpu));

unsafe extern "system" fn sys_allocator_alloc(_this: *mut ort_sys::OrtAllocator, _size: usize) -> *mut ::std::os::raw::c_void {
	ptr::null_mut()
}

unsafe extern "system" fn sys_allocator_free(_this: *mut ort_sys::OrtAllocator, p: *mut ::std::os::raw::c_void) {
	drop(CString::from_raw(p.cast()));
}

unsafe extern "system" fn sys_allocator_info(this_: *const ort_sys::OrtAllocator) -> *const ort_sys::OrtMemoryInfo {
	let _allocator = unsafe { &*this_.cast::<Allocator>() };
	(_allocator.memory_info as *const MemoryInfo).cast()
}

unsafe extern "system" fn sys_allocator_reserve(_this: *const ort_sys::OrtAllocator, _size: usize) -> *mut ::std::os::raw::c_void {
	ptr::null_mut()
}
