#![allow(deprecated)]

use std::{
    ffi::{CStr, NulError},
    fmt::{self, Display, Formatter},
};

use num_enum::{IntoPrimitive, TryFromPrimitive};
use singe_core::impl_enum_conversion;
use singe_nvml_sys as sys;
use thiserror::Error;

#[derive(Error, Debug, Clone, PartialEq, Eq)]
pub enum Error {
    #[error("nvml error ({code}): {message}")]
    Nvml { code: Status, message: String },

    #[error("string contains interior nul byte")]
    InteriorNul,

    #[error("unknown field value type ({0})")]
    UnknownFieldValueType(u32),

    #[error("unexpected {name} field value type ({value})")]
    UnexpectedFieldValueType { name: String, value: String },

    #[error("unknown {name} value ({value})")]
    UnknownEnumValue { name: String, value: u32 },

    #[error("negative {name} value ({value})")]
    NegativeValue { name: String, value: i64 },

    #[error("{name} returned no values")]
    EmptyOutput { name: String },
}

pub type Result<T> = std::result::Result<T, Error>;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive, IntoPrimitive)]
#[repr(u32)]
pub enum Status {
    Success = sys::nvmlReturn_t::NVML_SUCCESS as _,
    Uninitialized = sys::nvmlReturn_t::NVML_ERROR_UNINITIALIZED as _,
    InvalidArgument = sys::nvmlReturn_t::NVML_ERROR_INVALID_ARGUMENT as _,
    NotSupported = sys::nvmlReturn_t::NVML_ERROR_NOT_SUPPORTED as _,
    NoPermission = sys::nvmlReturn_t::NVML_ERROR_NO_PERMISSION as _,
    #[deprecated]
    AlreadyInitialized = sys::nvmlReturn_t::NVML_ERROR_ALREADY_INITIALIZED as _,
    NotFound = sys::nvmlReturn_t::NVML_ERROR_NOT_FOUND as _,
    InsufficientSize = sys::nvmlReturn_t::NVML_ERROR_INSUFFICIENT_SIZE as _,
    InsufficientPower = sys::nvmlReturn_t::NVML_ERROR_INSUFFICIENT_POWER as _,
    DriverNotLoaded = sys::nvmlReturn_t::NVML_ERROR_DRIVER_NOT_LOADED as _,
    Timeout = sys::nvmlReturn_t::NVML_ERROR_TIMEOUT as _,
    IrqIssue = sys::nvmlReturn_t::NVML_ERROR_IRQ_ISSUE as _,
    LibraryNotFound = sys::nvmlReturn_t::NVML_ERROR_LIBRARY_NOT_FOUND as _,
    FunctionNotFound = sys::nvmlReturn_t::NVML_ERROR_FUNCTION_NOT_FOUND as _,
    CorruptedInforom = sys::nvmlReturn_t::NVML_ERROR_CORRUPTED_INFOROM as _,
    GpuIsLost = sys::nvmlReturn_t::NVML_ERROR_GPU_IS_LOST as _,
    ResetRequired = sys::nvmlReturn_t::NVML_ERROR_RESET_REQUIRED as _,
    OperatingSystem = sys::nvmlReturn_t::NVML_ERROR_OPERATING_SYSTEM as _,
    LibRmVersionMismatch = sys::nvmlReturn_t::NVML_ERROR_LIB_RM_VERSION_MISMATCH as _,
    InUse = sys::nvmlReturn_t::NVML_ERROR_IN_USE as _,
    Memory = sys::nvmlReturn_t::NVML_ERROR_MEMORY as _,
    NoData = sys::nvmlReturn_t::NVML_ERROR_NO_DATA as _,
    VgpuEccNotSupported = sys::nvmlReturn_t::NVML_ERROR_VGPU_ECC_NOT_SUPPORTED as _,
    InsufficientResources = sys::nvmlReturn_t::NVML_ERROR_INSUFFICIENT_RESOURCES as _,
    FreqNotSupported = sys::nvmlReturn_t::NVML_ERROR_FREQ_NOT_SUPPORTED as _,
    ArgumentVersionMismatch = sys::nvmlReturn_t::NVML_ERROR_ARGUMENT_VERSION_MISMATCH as _,
    Deprecated = sys::nvmlReturn_t::NVML_ERROR_DEPRECATED as _,
    NotReady = sys::nvmlReturn_t::NVML_ERROR_NOT_READY as _,
    GpuNotFound = sys::nvmlReturn_t::NVML_ERROR_GPU_NOT_FOUND as _,
    InvalidState = sys::nvmlReturn_t::NVML_ERROR_INVALID_STATE as _,
    ResetTypeNotSupported = sys::nvmlReturn_t::NVML_ERROR_RESET_TYPE_NOT_SUPPORTED as _,
    Unknown = sys::nvmlReturn_t::NVML_ERROR_UNKNOWN as _,
}

impl_enum_conversion!(sys::nvmlReturn_t, Status);

impl Status {
    pub fn description(self) -> String {
        unsafe {
            let ptr = sys::nvmlErrorString(self.into());
            if ptr.is_null() {
                String::from("unknown nvml error")
            } else {
                CStr::from_ptr(ptr).to_string_lossy().into_owned()
            }
        }
    }
}

impl Display for Status {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            Self::Success => write!(f, "NVML_SUCCESS"),
            Self::Uninitialized => write!(f, "NVML_ERROR_UNINITIALIZED"),
            Self::InvalidArgument => write!(f, "NVML_ERROR_INVALID_ARGUMENT"),
            Self::NotSupported => write!(f, "NVML_ERROR_NOT_SUPPORTED"),
            Self::NoPermission => write!(f, "NVML_ERROR_NO_PERMISSION"),
            Self::AlreadyInitialized => write!(f, "NVML_ERROR_ALREADY_INITIALIZED"),
            Self::NotFound => write!(f, "NVML_ERROR_NOT_FOUND"),
            Self::InsufficientSize => write!(f, "NVML_ERROR_INSUFFICIENT_SIZE"),
            Self::InsufficientPower => write!(f, "NVML_ERROR_INSUFFICIENT_POWER"),
            Self::DriverNotLoaded => write!(f, "NVML_ERROR_DRIVER_NOT_LOADED"),
            Self::Timeout => write!(f, "NVML_ERROR_TIMEOUT"),
            Self::IrqIssue => write!(f, "NVML_ERROR_IRQ_ISSUE"),
            Self::LibraryNotFound => write!(f, "NVML_ERROR_LIBRARY_NOT_FOUND"),
            Self::FunctionNotFound => write!(f, "NVML_ERROR_FUNCTION_NOT_FOUND"),
            Self::CorruptedInforom => write!(f, "NVML_ERROR_CORRUPTED_INFOROM"),
            Self::GpuIsLost => write!(f, "NVML_ERROR_GPU_IS_LOST"),
            Self::ResetRequired => write!(f, "NVML_ERROR_RESET_REQUIRED"),
            Self::OperatingSystem => write!(f, "NVML_ERROR_OPERATING_SYSTEM"),
            Self::LibRmVersionMismatch => {
                write!(f, "NVML_ERROR_LIB_RM_VERSION_MISMATCH")
            }
            Self::InUse => write!(f, "NVML_ERROR_IN_USE"),
            Self::Memory => write!(f, "NVML_ERROR_MEMORY"),
            Self::NoData => write!(f, "NVML_ERROR_NO_DATA"),
            Self::VgpuEccNotSupported => write!(f, "NVML_ERROR_VGPU_ECC_NOT_SUPPORTED"),
            Self::InsufficientResources => write!(f, "NVML_ERROR_INSUFFICIENT_RESOURCES"),
            Self::FreqNotSupported => write!(f, "NVML_ERROR_FREQ_NOT_SUPPORTED"),
            Self::ArgumentVersionMismatch => {
                write!(f, "NVML_ERROR_ARGUMENT_VERSION_MISMATCH")
            }
            Self::Deprecated => write!(f, "NVML_ERROR_DEPRECATED"),
            Self::NotReady => write!(f, "NVML_ERROR_NOT_READY"),
            Self::GpuNotFound => write!(f, "NVML_ERROR_GPU_NOT_FOUND"),
            Self::InvalidState => write!(f, "NVML_ERROR_INVALID_STATE"),
            Self::ResetTypeNotSupported => write!(f, "NVML_ERROR_RESET_TYPE_NOT_SUPPORTED"),
            Self::Unknown => write!(f, "NVML_ERROR_UNKNOWN"),
        }
    }
}

impl From<NulError> for Error {
    fn from(_: NulError) -> Self {
        Self::InteriorNul
    }
}

impl From<sys::nvmlReturn_t> for Error {
    fn from(code: sys::nvmlReturn_t) -> Self {
        let message = unsafe {
            let c_ptr = sys::nvmlErrorString(code);
            if c_ptr.is_null() {
                String::from("unknown nvml error")
            } else {
                CStr::from_ptr(c_ptr).to_string_lossy().into_owned()
            }
        };

        Self::Nvml {
            code: code.into(),
            message,
        }
    }
}

impl From<Status> for Error {
    fn from(status: Status) -> Self {
        sys::nvmlReturn_t::from(status).into()
    }
}

#[macro_export]
macro_rules! try_ffi {
    ($expr:expr) => {{
        let err = { $expr };
        if err != singe_nvml_sys::nvmlReturn_t::NVML_SUCCESS {
            Err($crate::error::Error::from(err))
        } else {
            Ok(())
        }
    }};
}
