
use std::ffi::CString;
use std::sync::Once;

type VmpBeginFn = unsafe extern "C" fn(marker_name: *const i8);

type VmpBeginVirtualizationFn = unsafe extern "C" fn(marker_name: *const i8);

type VmpBeginMutationFn = unsafe extern "C" fn(marker_name: *const i8);

type VmpBeginUltraFn = unsafe extern "C" fn(marker_name: *const i8);

type VmpEndFn = unsafe extern "C" fn();

type VmpIsDebuggerPresentFn = unsafe extern "C" fn(check_kernel: i32) -> i32;

type VmpIsVirtualMachinePresentFn = unsafe extern "C" fn() -> i32;

type VmpDecryptStringAFn = unsafe extern "C" fn(value: *const i8) -> *const i8;

type VmpDecryptStringWFn = unsafe extern "C" fn(value: *const u16) -> *const u16;

type VmpFreeStringFn = unsafe extern "C" fn(value: *const std::ffi::c_void);

struct VmpSdk {
    begin: Option<VmpBeginFn>,
    begin_virtualization: Option<VmpBeginVirtualizationFn>,
    begin_mutation: Option<VmpBeginMutationFn>,
    begin_ultra: Option<VmpBeginUltraFn>,
    end: Option<VmpEndFn>,
    is_debugger_present: Option<VmpIsDebuggerPresentFn>,
    is_virtual_machine_present: Option<VmpIsVirtualMachinePresentFn>,
    decrypt_string_a: Option<VmpDecryptStringAFn>,
    decrypt_string_w: Option<VmpDecryptStringWFn>,
    free_string: Option<VmpFreeStringFn>,

    loaded: bool,
}

static INIT: Once = Once::new();
static mut SDK: VmpSdk = VmpSdk {
    begin: None,
    begin_virtualization: None,
    begin_mutation: None,
    begin_ultra: None,
    end: None,
    is_debugger_present: None,
    is_virtual_machine_present: None,
    decrypt_string_a: None,
    decrypt_string_w: None,
    free_string: None,
    loaded: false,
};

fn load_sdk() {

    #[inline(always)]
    fn sym(s: &str) -> Vec<u8> {
        let mut v = s.as_bytes().to_vec();
        v.push(0);
        v
    }

    INIT.call_once(|| {

        #[cfg(target_os = "windows")]
        let dll_name = obfstr::obfstr!("VMProtectSDK64.dll").to_string();
        #[cfg(target_os = "linux")]
        let dll_name = obfstr::obfstr!("libVMProtectSDK.so").to_string();
        #[cfg(not(any(target_os = "windows", target_os = "linux")))]
        let dll_name = obfstr::obfstr!("libVMProtectSDK.so").to_string();

        let lib = match unsafe { libloading::Library::new(&dll_name) } {
            Ok(lib) => lib,
            Err(_) => {

                return;
            }
        };

        unsafe {

            SDK.begin = lib
                .get::<VmpBeginFn>(&sym(obfstr::obfstr!("VMProtectBegin")))
                .ok()
                .map(|f| *f);
            SDK.begin_virtualization = lib
                .get::<VmpBeginVirtualizationFn>(&sym(obfstr::obfstr!("VMProtectBeginVirtualization")))
                .ok()
                .map(|f| *f);
            SDK.begin_mutation = lib
                .get::<VmpBeginMutationFn>(&sym(obfstr::obfstr!("VMProtectBeginMutation")))
                .ok()
                .map(|f| *f);
            SDK.begin_ultra = lib
                .get::<VmpBeginUltraFn>(&sym(obfstr::obfstr!("VMProtectBeginUltra")))
                .ok()
                .map(|f| *f);
            SDK.end = lib
                .get::<VmpEndFn>(&sym(obfstr::obfstr!("VMProtectEnd")))
                .ok()
                .map(|f| *f);
            SDK.is_debugger_present = lib
                .get::<VmpIsDebuggerPresentFn>(&sym(obfstr::obfstr!("VMProtectIsDebuggerPresent")))
                .ok()
                .map(|f| *f);
            SDK.is_virtual_machine_present = lib
                .get::<VmpIsVirtualMachinePresentFn>(&sym(obfstr::obfstr!("VMProtectIsVirtualMachinePresent")))
                .ok()
                .map(|f| *f);
            SDK.decrypt_string_a = lib
                .get::<VmpDecryptStringAFn>(&sym(obfstr::obfstr!("VMProtectDecryptStringA")))
                .ok()
                .map(|f| *f);
            SDK.decrypt_string_w = lib
                .get::<VmpDecryptStringWFn>(&sym(obfstr::obfstr!("VMProtectDecryptStringW")))
                .ok()
                .map(|f| *f);
            SDK.free_string = lib
                .get::<VmpFreeStringFn>(&sym(obfstr::obfstr!("VMProtectFreeString")))
                .ok()
                .map(|f| *f);

            SDK.loaded = true;

            std::mem::forget(lib);
        }
    });
}

pub fn is_sdk_loaded() -> bool {
    load_sdk();
    unsafe { SDK.loaded }
}

pub fn vmp_begin(marker: &str) {
    load_sdk();
    if let Some(f) = unsafe { SDK.begin } {
        if let Ok(name) = CString::new(marker) {
            unsafe { f(name.as_ptr()) };
        }
    }
}

pub fn vmp_begin_virtualization(marker: &str) {
    load_sdk();
    if let Some(f) = unsafe { SDK.begin_virtualization } {
        if let Ok(name) = CString::new(marker) {
            unsafe { f(name.as_ptr()) };
        }
    }
}

pub fn vmp_begin_mutation(marker: &str) {
    load_sdk();
    if let Some(f) = unsafe { SDK.begin_mutation } {
        if let Ok(name) = CString::new(marker) {
            unsafe { f(name.as_ptr()) };
        }
    }
}

pub fn vmp_begin_ultra(marker: &str) {
    load_sdk();
    if let Some(f) = unsafe { SDK.begin_ultra } {
        if let Ok(name) = CString::new(marker) {
            unsafe { f(name.as_ptr()) };
        }
    }
}

pub fn vmp_end() {
    load_sdk();
    if let Some(f) = unsafe { SDK.end } {
        unsafe { f() };
    }
}

pub fn is_debugger_present(check_kernel: bool) -> bool {
    load_sdk();
    if let Some(f) = unsafe { SDK.is_debugger_present } {
        unsafe { f(if check_kernel { 1 } else { 0 }) != 0 }
    } else {
        false
    }
}

pub fn is_virtual_machine() -> bool {
    load_sdk();
    if let Some(f) = unsafe { SDK.is_virtual_machine_present } {
        unsafe { f() != 0 }
    } else {
        false
    }
}

pub fn decrypt_string(value: &str) -> String {
    load_sdk();
    if let Some(f) = unsafe { SDK.decrypt_string_a } {
        if let Ok(cstr) = CString::new(value) {
            let decrypted = unsafe { f(cstr.as_ptr()) };
            if !decrypted.is_null() {
                let result = unsafe { std::ffi::CStr::from_ptr(decrypted) }
                    .to_string_lossy()
                    .into_owned();

                if let Some(free_fn) = unsafe { SDK.free_string } {
                    unsafe { free_fn(decrypted as *const std::ffi::c_void) };
                }
                return result;
            }
        }
    }

    value.to_string()
}

pub fn anti_debug_check<F: FnOnce()>(check_kernel: bool, on_detected: F) {
    if is_debugger_present(check_kernel) {
        on_detected();
    }
}

pub fn anti_vm_check<F: FnOnce()>(on_detected: F) {
    if is_virtual_machine() {
        on_detected();
    }
}

#[cfg(feature = "vmprotect")]
#[macro_export]
macro_rules! vmp_protect {
    ($marker:expr, Ultra, $body:block) => {{
        $crate::utils::protection::vmp_begin_ultra($marker);
        let __vmp_result = (|| $body)();
        $crate::utils::protection::vmp_end();
        __vmp_result
    }};
    ($marker:expr, Mutation, $body:block) => {{
        $crate::utils::protection::vmp_begin_mutation($marker);
        let __vmp_result = (|| $body)();
        $crate::utils::protection::vmp_end();
        __vmp_result
    }};
    ($marker:expr, Virtualization, $body:block) => {{
        $crate::utils::protection::vmp_begin_virtualization($marker);
        let __vmp_result = (|| $body)();
        $crate::utils::protection::vmp_end();
        __vmp_result
    }};
}
