#![allow(non_snake_case)]

use std::error;
use std::fmt;
use std::ptr;

mod c {
    use std::ffi::c_void;
    use std::os::raw::c_ulong;

    pub type DWORD = c_ulong;
    pub type HANDLE = LPVOID;
    pub type LPVOID = *mut c_void;
    pub type HINSTANCE = HANDLE;
    pub type HMODULE = HINSTANCE;
    pub type WCHAR = u16;
    pub type LPCWSTR = *const WCHAR;
    pub type LPWSTR = *mut WCHAR;

    pub const FACILITY_NT_BIT: DWORD = 0x1000_0000;
    pub const FORMAT_MESSAGE_FROM_HMODULE: DWORD = 0x00000800;
    pub const FORMAT_MESSAGE_FROM_SYSTEM: DWORD = 0x00001000;
    pub const FORMAT_MESSAGE_IGNORE_INSERTS: DWORD = 0x00000200;

    extern "system" {
        pub fn GetLastError() -> DWORD;
        pub fn GetModuleHandleW(lpModuleName: LPCWSTR) -> HMODULE;
        pub fn FormatMessageW(
            flags: DWORD,
            lpSrc: LPVOID,
            msgId: DWORD,
            langId: DWORD,
            buf: LPWSTR,
            nsize: DWORD,
            args: *const c_void,
        ) -> DWORD;
    }
}

macro_rules! last_ident {
    ($i:ident) => {
        stringify!($i)
    };
    ($start:ident, $($cont:ident),+) => {
        last_ident!($($cont),+)
    };
}

macro_rules! os_call {
    ($($path:ident)::+ ($($args:expr),*), $success:expr) => {
        {
            let result = unsafe{ $($path)::+ ($($args),*) };
            if !($success)(result) {
                let name = last_ident!($($path),+);
                let err_code = $crate::win::errno();
                Err($crate::win::OsError{
                    function: name,
                    error_code: err_code as u32,
                    message: $crate::win::error_string(err_code)
                })?;
            }
            result
        }
    };
}

#[derive(Debug)]
pub struct OsError {
    pub function: &'static str,
    pub error_code: u32,
    pub message: String,
}

impl fmt::Display for OsError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:?}", self)
    }
}

impl error::Error for OsError {
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        None
    }
}

pub fn errno() -> i32 {
    unsafe { c::GetLastError() as i32 }
}

/// Gets a detailed string description for the given error number.
pub fn error_string(mut errnum: i32) -> String {
    // This value is calculated from the macro
    // MAKELANGID(LANG_SYSTEM_DEFAULT, SUBLANG_SYS_DEFAULT)
    let langId = 0x0800 as c::DWORD;

    let mut buf = [0 as c::WCHAR; 2048];

    unsafe {
        let mut module = ptr::null_mut();
        let mut flags = 0;

        // NTSTATUS errors may be encoded as HRESULT, which may returned from
        // GetLastError. For more information about Windows error codes, see
        // `[MS-ERREF]`: https://msdn.microsoft.com/en-us/library/cc231198.aspx
        if (errnum & c::FACILITY_NT_BIT as i32) != 0 {
            // format according to https://support.microsoft.com/en-us/help/259693
            const NTDLL_DLL: &[u16] = &[
                'N' as _, 'T' as _, 'D' as _, 'L' as _, 'L' as _, '.' as _, 'D' as _, 'L' as _,
                'L' as _, 0,
            ];
            module = c::GetModuleHandleW(NTDLL_DLL.as_ptr());

            if module != ptr::null_mut() {
                errnum ^= c::FACILITY_NT_BIT as i32;
                flags = c::FORMAT_MESSAGE_FROM_HMODULE;
            }
        }

        let res = c::FormatMessageW(
            flags | c::FORMAT_MESSAGE_FROM_SYSTEM | c::FORMAT_MESSAGE_IGNORE_INSERTS,
            module,
            errnum as c::DWORD,
            langId,
            buf.as_mut_ptr(),
            buf.len() as c::DWORD,
            ptr::null(),
        ) as usize;
        if res == 0 {
            // Sometimes FormatMessageW can fail e.g., system doesn't like langId,
            let fm_err = errno();
            return format!(
                "OS Error {} (FormatMessageW() returned error {})",
                errnum, fm_err
            );
        }

        match String::from_utf16(&buf[..res]) {
            Ok(mut msg) => {
                // Trim trailing CRLF inserted by FormatMessageW
                let len = msg.trim_end().len();
                msg.truncate(len);
                msg
            }
            Err(..) => format!(
                "OS Error {} (FormatMessageW() returned \
                 invalid UTF-16)",
                errnum
            ),
        }
    }
}
