extern crate gl;
extern crate khronos_egl as egl;
extern crate wayland_client;
extern crate wayland_egl;
extern crate wayland_protocols;
extern crate libloading;

use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::ptr;
use std::ffi::CStr;
use gl::types::{GLint, GLuint, GLchar, GLenum, GLboolean, GLvoid};

use wayland_client::{
	protocol::{wl_compositor::WlCompositor, wl_surface::WlSurface},
	DispatchData, Display, EventQueue, Main
};

use wayland_protocols::xdg_shell::client::{
	xdg_surface::{self, XdgSurface},
	xdg_wm_base::{self, XdgWmBase},
};

fn process_xdg_event(xdg: Main<XdgWmBase>, event: xdg_wm_base::Event, _dd: DispatchData) {
	use xdg_wm_base::Event::*;

	match event {
		Ping { serial } => xdg.pong(serial),
		_ => (),
	}
}

struct DisplayConnection {
	display: Display,
	event_queue: EventQueue,
	compositor: Main<WlCompositor>,
	xdg: Main<XdgWmBase>,
}

fn setup_wayland() -> DisplayConnection {
	let display =
		wayland_client::Display::connect_to_env().expect("unable to connect to the wayland server");
	let mut event_queue = display.create_event_queue();
	let attached_display = display.clone().attach(event_queue.token());

	let globals = wayland_client::GlobalManager::new(&attached_display);

	// Roundtrip to retrieve the globals list
	event_queue
		.sync_roundtrip(&mut (), |_, _, _| unreachable!())
		.unwrap();

	// Get the compositor.
	let compositor: Main<WlCompositor> = globals.instantiate_exact(1).unwrap();

	// Xdg protocol.
	let xdg: Main<XdgWmBase> = globals.instantiate_exact(1).unwrap();
	xdg.quick_assign(process_xdg_event);

	DisplayConnection {
		display,
		event_queue,
		compositor,
		xdg,
	}
}

fn setup_egl(egl: &egl::DynamicInstance, display: &Display) -> egl::Display {
	let egl_display = egl.get_display(display.get_display_ptr() as *mut std::ffi::c_void).unwrap();
	egl.initialize(egl_display).unwrap();

	egl_display
}

fn create_context(egl: &egl::DynamicInstance, display: egl::Display) -> (egl::Context, egl::Config) {
	let attributes = [
		egl::RED_SIZE,
		8,
		egl::GREEN_SIZE,
		8,
		egl::BLUE_SIZE,
		8,
		egl::NONE,
	];

	let config = egl.choose_first_config(display, &attributes)
		.expect("unable to choose an EGL configuration")
		.expect("no EGL configuration found");

	let context_attributes = [
		egl::CONTEXT_MAJOR_VERSION,
		4,
		egl::CONTEXT_MINOR_VERSION,
		0,
		egl::CONTEXT_OPENGL_PROFILE_MASK,
		egl::CONTEXT_OPENGL_CORE_PROFILE_BIT,
		egl::NONE,
	];

	let context = egl.create_context(display, config, None, &context_attributes)
		.expect("unable to create an EGL context");

	(context, config)
}

struct Surface {
	handle: Main<WlSurface>,
	initialized: AtomicBool
}

fn create_surface(
	egl: &Arc<egl::DynamicInstance>,
	ctx: &DisplayConnection,
	egl_display: egl::Display,
	egl_context: egl::Context,
	egl_config: egl::Config,
	width: i32,
	height: i32,
) -> Arc<Surface> {
	let wl_surface = ctx.compositor.create_surface();
	let xdg_surface = ctx.xdg.get_xdg_surface(&wl_surface);

	let xdg_toplevel = xdg_surface.get_toplevel();
	xdg_toplevel.set_app_id("khronos-egl-test".to_string());
	xdg_toplevel.set_title("Test".to_string());

	wl_surface.commit();
	ctx.display.flush().unwrap();

	let surface = Arc::new(Surface {
		handle: wl_surface,
		initialized: AtomicBool::new(false)
	});

	let weak_surface = Arc::downgrade(&surface);

	let egl = egl.clone();
	xdg_surface.quick_assign(move |xdg_surface: Main<XdgSurface>, event: xdg_surface::Event, _dd: DispatchData| {
		use xdg_surface::Event::*;

		match event {
			Configure { serial } => {
				if let Some(surface) = weak_surface.upgrade() {
					if !surface.initialized.swap(true, Ordering::Relaxed) {
						let wl_egl_surface = wayland_egl::WlEglSurface::new(&surface.handle, width, height);

						let egl_surface = unsafe {
							egl.create_window_surface(
								egl_display,
								egl_config,
								wl_egl_surface.ptr() as egl::NativeWindowType,
								None,
							)
							.expect("unable to create an EGL surface")
						};

						egl.make_current(egl_display, Some(egl_surface), Some(egl_surface), Some(egl_context))
							.expect("unable to bind the context");

						render();

						egl.swap_buffers(egl_display, egl_surface)
							.expect("unable to post the surface content");

						xdg_surface.ack_configure(serial);
					}
				}
			},
			_ => (),
		}
	});

	surface
}

fn main() {
	let egl = unsafe { Arc::new(egl::DynamicInstance::<egl::EGL1_5>::load_required().expect("unable to load libEGL.so.1")) };

	// Setup Open GL.
	egl.bind_api(egl::OPENGL_API)
		.expect("unable to select OpenGL API");
	gl::load_with(|name| egl.get_proc_address(name).unwrap() as *const std::ffi::c_void);

	// Setup the Wayland client.
	let mut ctx = setup_wayland();

	// Setup EGL.
	let egl_display = setup_egl(&egl, &ctx.display);
	let (egl_context, egl_config) = create_context(&egl, egl_display);

	// Create a surface.
	// Note that it must be kept alive to the end of execution.
	let _surface = create_surface(&egl, &ctx, egl_display, egl_context, egl_config, 800, 600);

	loop {
		ctx.event_queue
			.dispatch(&mut (), |_, _, _| { /* we ignore unfiltered messages */ })
			.unwrap();
	}
}

const VERTEX: &'static [GLint; 8] = &[
	-1, -1,
	1, -1,
	1, 1,
	-1, 1
];

const INDEXES: &'static [GLuint; 4] = &[
	0, 1, 2, 3
];

const VERTEX_SHADER: &[u8] = b"#version 400
in vec2 position;

void main() {
	gl_Position = vec4(position, 0.0f, 1.0f);
}
\0";

const FRAGMENT_SHADER: &[u8] = b"#version 400
out vec4 color;

void main() {
	color = vec4(1.0f, 0.0f, 0.0f, 1.0f);
}
\0";

fn render() {
	unsafe {
		let vertex_shader = gl::CreateShader(gl::VERTEX_SHADER);
		check_gl_errors();
		let src = CStr::from_bytes_with_nul_unchecked(VERTEX_SHADER).as_ptr();
		gl::ShaderSource(vertex_shader, 1, (&[src]).as_ptr(), ptr::null());
		check_gl_errors();
		gl::CompileShader(vertex_shader);
		check_shader_status(vertex_shader);

		let fragment_shader = gl::CreateShader(gl::FRAGMENT_SHADER);
		check_gl_errors();
		let src = CStr::from_bytes_with_nul_unchecked(FRAGMENT_SHADER).as_ptr();
		gl::ShaderSource(fragment_shader, 1, (&[src]).as_ptr(), ptr::null());
		check_gl_errors();
		gl::CompileShader(fragment_shader);
		check_shader_status(fragment_shader);

		let program = gl::CreateProgram();
		check_gl_errors();
		gl::AttachShader(program, vertex_shader);
		check_gl_errors();
		gl::AttachShader(program, fragment_shader);
		check_gl_errors();
		gl::LinkProgram(program);
		check_gl_errors();
		gl::UseProgram(program);
		check_gl_errors();

		let mut buffer = 0;
		gl::GenBuffers(1, &mut buffer);
		check_gl_errors();
		gl::BindBuffer(gl::ARRAY_BUFFER, buffer);
		check_gl_errors();
		gl::BufferData(
			gl::ARRAY_BUFFER,
			8 * 4,
			VERTEX.as_ptr() as *const std::ffi::c_void,
			gl::STATIC_DRAW
		);
		check_gl_errors();

		let mut vertex_input = 0;
		gl::GenVertexArrays(1, &mut vertex_input);
		check_gl_errors();
		gl::BindVertexArray(vertex_input);
		check_gl_errors();
		gl::EnableVertexAttribArray(0);
		check_gl_errors();
		gl::VertexAttribPointer(
			0, 2, gl::INT, gl::FALSE as GLboolean, 0, 0 as *const GLvoid
		);
		check_gl_errors();

		let mut indexes = 0;
		gl::GenBuffers(1, &mut indexes);
		check_gl_errors();
		gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, indexes);
		check_gl_errors();
		gl::BufferData(
			gl::ELEMENT_ARRAY_BUFFER,
			4 * 4,
			INDEXES.as_ptr() as *const std::ffi::c_void,
			gl::STATIC_DRAW
		);
		check_gl_errors();

		gl::DrawElements(gl::TRIANGLE_FAN, 4, gl::UNSIGNED_INT, std::ptr::null());
		check_gl_errors();
	}
}

fn format_error(e: GLenum) -> &'static str {
	 match e {
		  gl::NO_ERROR => "No error",
		  gl::INVALID_ENUM => "Invalid enum",
		  gl::INVALID_VALUE => "Invalid value",
		  gl::INVALID_OPERATION => "Invalid operation",
		  gl::INVALID_FRAMEBUFFER_OPERATION => "Invalid framebuffer operation",
		  gl::OUT_OF_MEMORY => "Out of memory",
		  gl::STACK_UNDERFLOW => "Stack underflow",
		  gl::STACK_OVERFLOW => "Stack overflow",
		  _ => "Unknown error"
	 }
}

pub fn check_gl_errors() {
	 unsafe {
		  match gl::GetError() {
				gl::NO_ERROR => (),
				e => {
					panic!("OpenGL error: {}", format_error(e))
				}
		  }
	 }
}

unsafe fn check_shader_status(shader: GLuint) {
	let mut status = gl::FALSE as GLint;
	gl::GetShaderiv(shader, gl::COMPILE_STATUS, &mut status);
	if status != (gl::TRUE as GLint) {
		let mut len = 0;
		gl::GetProgramiv(shader, gl::INFO_LOG_LENGTH, &mut len);
		if len > 0 {
			let mut buf = Vec::with_capacity(len as usize);
			buf.set_len((len as usize) - 1); // subtract 1 to skip the trailing null character
			gl::GetProgramInfoLog(
				shader,
				len,
				ptr::null_mut(),
				buf.as_mut_ptr() as *mut GLchar,
			);

			let log = String::from_utf8(buf).unwrap();
			eprintln!("shader compilation log:\n{}", log);
		}

		panic!("shader compilation failed");
	}
}
