use std::sync::{Arc, Mutex};
use std::os::raw::{c_int, c_void};
use std::ffi::{CStr, CString};
use std::ptr::null_mut;
use std::thread;

use vst::plugin::HostCallback;
use vst::host::Host;
use x11::glx::*;
use x11::xlib;
use xcb::dri2;
use log::*;
use rand::Rng;

use crate::parameters::Parameters;

type GlXCreateContextAttribsARBProc =
unsafe extern "C" fn (dpy: *mut xlib::Display, fbc: GLXFBConfig,
                      share_context: GLXContext, direct: xlib::Bool,
                      attribs: *const c_int) -> GLXContext;

static mut ctx_error_occurred: bool = false;
unsafe extern "C" fn ctx_error_handler(
    _dpy: *mut xlib::Display,
    _ev: *mut xlib::XErrorEvent) -> i32 {
    ctx_error_occurred = true;
    0
}

const GLX_CONTEXT_MAJOR_VERSION_ARB: u32 = 0x2091;
const GLX_CONTEXT_MINOR_VERSION_ARB: u32 = 0x2092;

pub struct Window {
    t: thread::JoinHandle<()>,
}

impl Window {
    pub fn new(host_callback: Arc<Mutex<HostCallback>>, params: Arc<Parameters>, parent: *mut std::ffi::c_void) -> Self {
        let parent_id = parent as u32;
        let t = thread::spawn(move || {
            let (conn, screen_num) = xcb::Connection::connect_with_xlib_display().unwrap();
            //conn.set_event_queue_owner(xcb::EventQueueOwner::Xcb); // TODO: need this?

            if glx_dec_version(conn.get_raw_dpy()) < 13 {
                panic!("glx-1.3 is not supported");
            }

            let fbc = get_glxfbconfig(conn.get_raw_dpy(), screen_num, &[
                GLX_X_RENDERABLE    , 1,
                GLX_DRAWABLE_TYPE   , GLX_WINDOW_BIT,
                GLX_RENDER_TYPE     , GLX_RGBA_BIT,
                GLX_X_VISUAL_TYPE   , GLX_TRUE_COLOR,
                GLX_RED_SIZE        , 8,
                GLX_GREEN_SIZE      , 8,
                GLX_BLUE_SIZE       , 8,
                GLX_ALPHA_SIZE      , 8,
                GLX_DEPTH_SIZE      , 24,
                GLX_STENCIL_SIZE    , 8,
                GLX_DOUBLEBUFFER    , 1,
                0
            ]);

            let vi: *const xlib::XVisualInfo = unsafe {
                glXGetVisualFromFBConfig(conn.get_raw_dpy(), fbc)
            };

            let dri2_ev = {
                conn.prefetch_extension_data(dri2::id());
                match conn.get_extension_data(dri2::id()) {
                    None => { panic!("could not load dri2 extension") },
                    Some(r) => { r.first_event() }
                }
            };

            let (wm_protocols, wm_delete_window) = {
                let pc = xcb::intern_atom(&conn, false, "WM_PROTOCOLS");
                let dwc = xcb::intern_atom(&conn, false, "WM_DELETE_WINDOW");

                let p = match pc.get_reply() {
                    Ok(p) => p.atom(),
                    Err(_) => panic!("could not load WM_PROTOCOLS atom")
                };
                let dw = match dwc.get_reply() {
                    Ok(dw) => dw.atom(),
                    Err(_) => panic!("could not load WM_DELETE_WINDOW atom")
                };
                (p, dw)
            };

            let setup = conn.get_setup();
            let screen = unsafe {
                setup.roots().nth((*vi).screen as usize).unwrap()
            };

            let cmap = conn.generate_id();
            let win = conn.generate_id();

            unsafe {
                xcb::create_colormap(&conn, xcb::COLORMAP_ALLOC_NONE as u8,
                                     cmap, parent_id, (*vi).visualid as u32);
            }

            let cw_values = [
                (xcb::CW_BACK_PIXEL, screen.white_pixel()),
                (xcb::CW_BORDER_PIXEL, screen.black_pixel()),
                (xcb::CW_EVENT_MASK,
                 xcb::EVENT_MASK_KEY_PRESS | xcb::EVENT_MASK_EXPOSURE),
                (xcb::CW_COLORMAP, cmap)
            ];

            unsafe {
                xcb::create_window(&conn, (*vi).depth as u8, win, parent_id, 0, 0, 1024, 1024,
                                   0, xcb::WINDOW_CLASS_INPUT_OUTPUT as u16,
                                   (*vi).visualid as u32, &cw_values);
            }

            unsafe {
                xlib::XFree(vi as *mut c_void);
            }

            let title = "XCB OpenGL";
            xcb::change_property(&conn,
                                 xcb::PROP_MODE_REPLACE as u8,
                                 win,
                                 xcb::ATOM_WM_NAME,
                                 xcb::ATOM_STRING,
                                 8, title.as_bytes());

            let protocols = [wm_delete_window];
            xcb::change_property(&conn, xcb::PROP_MODE_REPLACE as u8,
                                 win, wm_protocols, xcb::ATOM_ATOM, 32, &protocols);

            xcb::map_window(&conn, win);
            conn.flush();
            unsafe {
                xlib::XSync(conn.get_raw_dpy(), xlib::False);
            }

            let glx_exts = unsafe {
                CStr::from_ptr(
                    glXQueryExtensionsString(conn.get_raw_dpy(), screen_num))
                    .to_str().unwrap()
            };

            if !check_glx_extension(&glx_exts, "GLX_ARB_create_context") {
                panic!("could not find GLX extension GLX_ARB_create_context");
            }

            // with glx, no need of a current context is needed to load symbols
            // otherwise we would need to create a temporary legacy GL context
            // for loading symbols (at least glXCreateContextAttribsARB)
            let glx_create_context_attribs: GlXCreateContextAttribsARBProc = unsafe {
                std::mem::transmute(load_gl_func("glXCreateContextAttribsARB"))
            };

            // loading all other symbols
            unsafe {
                gl::load_with(|n| load_gl_func(&n));
            }

            if !gl::GenVertexArrays::is_loaded() {
                panic!("no GL3 support available!");
            }

            // installing an event handler to check if error is generated
            unsafe {
                ctx_error_occurred = false;
            }
            let old_handler = unsafe {
                xlib::XSetErrorHandler(Some(ctx_error_handler))
            };

            let context_attribs: [c_int; 5] = [
                GLX_CONTEXT_MAJOR_VERSION_ARB as c_int, 3,
                GLX_CONTEXT_MINOR_VERSION_ARB as c_int, 0,
                0
            ];
            let ctx = unsafe {
                glx_create_context_attribs(conn.get_raw_dpy(), fbc, null_mut(),
                                           xlib::True, &context_attribs[0] as *const c_int)
            };

            conn.flush();
            unsafe {
                xlib::XSync(conn.get_raw_dpy(), xlib::False);
                xlib::XSetErrorHandler(std::mem::transmute(old_handler));
            }

            unsafe {
                if ctx.is_null() || ctx_error_occurred {
                    panic!("error when creating gl-3.0 context");
                }

                if glXIsDirect(conn.get_raw_dpy(), ctx) == 0 {
                    panic!("obtained indirect rendering context")
                }
            }

            handle_events(
                host_callback,
                params,
                Arc::new(conn),
                win,
                ctx,
                wm_protocols,
                wm_delete_window,
                dri2_ev,
            );
        });

        Self {
            t,
        }
    }

    pub fn get_width(&self) -> u32 {
        1024
    }

    pub fn get_height(&self) -> u32 {
        1024
    }
}


// returns the glx version in a decimal form
// eg. 1.3  => 13
fn glx_dec_version(dpy: *mut xlib::Display) -> i32 {
    let mut maj: c_int = 0;
    let mut min: c_int = 0;
    unsafe {
        if glXQueryVersion(dpy,
                           &mut maj as *mut c_int,
                           &mut min as *mut c_int) == 0 {
            panic!("cannot get glx version");
        }
    }
    (maj*10 + min) as i32
}

fn get_glxfbconfig(dpy: *mut xlib::Display, screen_num: i32,
                   visual_attribs: &[i32]) -> GLXFBConfig {
    unsafe {
        let mut fbcount: c_int = 0;
        let fbcs = glXChooseFBConfig(dpy, screen_num,
                                     visual_attribs.as_ptr(),
                                     &mut fbcount as *mut c_int);

        if fbcount == 0 {
            panic!("could not find compatible fb config");
        }
        // we pick the first from the list
        let fbc = *fbcs;
        xlib::XFree(fbcs as *mut c_void);
        fbc
    }
}

fn check_glx_extension(glx_exts: &str, ext_name: &str) -> bool {
    for glx_ext in glx_exts.split(" ") {
        if glx_ext == ext_name {
            return true;
        }
    }
    false
}

unsafe fn load_gl_func (name: &str) -> *mut c_void {
    let cname = CString::new(name).unwrap();
    let ptr: *mut c_void = std::mem::transmute(glXGetProcAddress(
        cname.as_ptr() as *const u8
    ));
    if ptr.is_null() {
        panic!("could not load {}", name);
    }
    ptr
}

unsafe fn check_gl_error() {
    let err = gl::GetError();
    if err != gl::NO_ERROR {
        println!("got gl error {}", err);
    }
}

fn handle_events(
    host_callback: Arc<Mutex<HostCallback>>,
    params: Arc<Parameters>,
    conn: Arc<xcb::Connection>,
    win: u32,
    ctx: *mut x11::glx::__GLXcontextRec,
    wm_protocols: u32,
    wm_delete_window: u32,
    dri2_ev: u8,
) {
    let mut rng = rand::thread_rng();
    loop {
        info!("Event loop begin");
        if let Some(ev) = conn.wait_for_event() {
            let ev_type = ev.response_type() & !0x80;
            match ev_type {
                xcb::EXPOSE => {
                    unsafe {
                        glXMakeCurrent(conn.get_raw_dpy(), win as xlib::XID, ctx);
                        gl::ClearColor(0.5f32, 0.5f32, 1.0f32, 1.0f32);
                        gl::Clear(gl::COLOR_BUFFER_BIT);
                        gl::Flush();
                        check_gl_error();
                        glXSwapBuffers(conn.get_raw_dpy(), win as xlib::XID);
                        glXMakeCurrent(conn.get_raw_dpy(), 0, null_mut());
                    }
                },
                xcb::KEY_PRESS => {
                    let new_param_val = rng.gen_range(0.0, 1.0);
                    params.pulse_width.set(new_param_val);
                    host_callback.lock().unwrap().automate(1, new_param_val);
                    // break;
                },
                xcb::CLIENT_MESSAGE => {
                    let cmev = unsafe {
                        xcb::cast_event::<xcb::ClientMessageEvent>(&ev)
                    };
                    if cmev.type_() == wm_protocols && cmev.format() == 32 {
                        let protocol = cmev.data().data32()[0];
                        if protocol == wm_delete_window {
                            break;
                        }
                    }
                },
                _ => {
                    // following stuff is not obvious at all, but is necessary
                    // to handle GL when XCB owns the event queue
                    if ev_type == dri2_ev || ev_type == dri2_ev+1 {
                        // these are libgl dri2 event that need special handling
                        // see https://bugs.freedesktop.org/show_bug.cgi?id=35945#c4
                        // and mailing thread starting here:
                        // http://lists.freedesktop.org/archives/xcb/2015-November/010556.html

                        unsafe {
                            if let Some(proc_) =
                            xlib::XESetWireToEvent(conn.get_raw_dpy(),
                                                   ev_type as i32, None) {
                                xlib::XESetWireToEvent(conn.get_raw_dpy(),
                                                       ev_type as i32, Some(proc_));
                                let raw_ev = ev.ptr;
                                (*raw_ev).sequence =
                                    xlib::XLastKnownRequestProcessed(
                                        conn.get_raw_dpy()) as u16;
                                let mut dummy: xlib::XEvent = std::mem::zeroed();
                                proc_(conn.get_raw_dpy(),
                                      &mut dummy as *mut xlib::XEvent,
                                      raw_ev as *mut xlib::xEvent);
                            }
                        }
                    }
                }
            }
            conn.flush();
        }
        else {
            break;
        }

        info!("Event loop end");
    }
}