use std::ffi::c_void;
use std::io::{stderr, Write};
use std::panic::catch_unwind;
use std::ptr::null_mut;
use std::sync::Mutex;

use crate::opengl::util::gl_enum;

pub type MessageCallback = fn(
    source: MessageSource,
    ty: MessageType,
    id: u32,
    severity: MessageSeverity,
    message: &str,
);

pub fn enable_debug_output()
{
    unsafe {
        gl::Enable(gl::DEBUG_OUTPUT);
        gl::Enable(gl::DEBUG_OUTPUT_SYNCHRONOUS);
    }
}

pub fn set_debug_message_callback(cb: MessageCallback)
{
    *DEBUG_MESSAGE_CB.lock().unwrap() = Some(cb);

    unsafe {
        gl::DebugMessageCallback(Some(debug_message_cb), null_mut());
    }
}

pub fn set_debug_message_control(
    source: Option<MessageSource>,
    ty: Option<MessageType>,
    severity: Option<MessageSeverity>,
    ids: &[u32],
    ids_action: MessageIdsAction,
)
{
    // Ids shouldn't realistically be large enough to cause a panic here
    let ids_len: i32 = ids.len().try_into().unwrap();

    unsafe {
        gl::DebugMessageControl(
            source.map_or(gl::DONT_CARE, |source| source as u32),
            ty.map_or(gl::DONT_CARE, |ty| ty as u32),
            severity.map_or(gl::DONT_CARE, |severity| severity as u32),
            ids_len,
            ids.as_ptr(),
            ids_action as u8,
        );
    }
}

#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub enum MessageIdsAction
{
    Enable = 1,
    Disable = 0,
}

gl_enum! {
pub enum MessageSource
{
    Api = gl::DEBUG_SOURCE_API,
    WindowSystem = gl::DEBUG_SOURCE_WINDOW_SYSTEM,
    ShaderCompiler = gl::DEBUG_SOURCE_SHADER_COMPILER,
    ThirdParty = gl::DEBUG_SOURCE_THIRD_PARTY,
    Application = gl::DEBUG_SOURCE_APPLICATION,
    Other = gl::DEBUG_SOURCE_OTHER,
}
}

gl_enum! {
pub enum MessageType
{
    DeprecatedBehavior = gl::DEBUG_TYPE_DEPRECATED_BEHAVIOR,
    Error = gl::DEBUG_TYPE_ERROR,
    Marker = gl::DEBUG_TYPE_MARKER,
    Other = gl::DEBUG_TYPE_OTHER,
    Performance = gl::DEBUG_TYPE_PERFORMANCE,
    PopGroup = gl::DEBUG_TYPE_POP_GROUP,
    PushGroup = gl::DEBUG_TYPE_PUSH_GROUP,
    Portability = gl::DEBUG_TYPE_PORTABILITY,
    UndefinedBehavior = gl::DEBUG_TYPE_UNDEFINED_BEHAVIOR,
}
}

gl_enum! {
pub enum MessageSeverity
{
    High = gl::DEBUG_SEVERITY_HIGH,
    Medium = gl::DEBUG_SEVERITY_MEDIUM,
    Low = gl::DEBUG_SEVERITY_LOW,
    Notification = gl::DEBUG_SEVERITY_NOTIFICATION,
}
}

static DEBUG_MESSAGE_CB: Mutex<Option<MessageCallback>> = Mutex::new(None);

extern "system" fn debug_message_cb(
    source: gl::types::GLenum,
    ty: gl::types::GLenum,
    id: gl::types::GLuint,
    severity: gl::types::GLenum,
    message_length: gl::types::GLsizei,
    message: *const gl::types::GLchar,
    _user_param: *mut c_void,
)
{
    // Unwinds are catched because unwinding from Rust code into foreign code is UB.
    let res = catch_unwind(|| {
        let cb_lock = DEBUG_MESSAGE_CB.lock().unwrap();

        if let Some(cb) = *cb_lock {
            let msg_source = MessageSource::from_gl(source).unwrap();
            let msg_type = MessageType::from_gl(ty).unwrap();
            let msg_severity = MessageSeverity::from_gl(severity).unwrap();

            let msg_length = usize::try_from(message_length).unwrap();

            // SAFETY: The received message should be a valid ASCII string
            let message = unsafe {
                std::str::from_utf8_unchecked(std::slice::from_raw_parts(
                    message.cast(),
                    msg_length,
                ))
            };

            cb(msg_source, msg_type, id, msg_severity, message);
        }
    });

    if res.is_err() {
        // eprintln is not used since it can panic and unwinds are unwanted because
        // unwinding from Rust code into foreign code is UB.
        stderr()
            .write_all(b"ERROR: Panic in debug message callback")
            .ok();
        println!();
    }
}