#include <cvd/glwindow.h>
#include <exception>

#include <GL/glx.h>
#include <X11/Xlib.h>
#include <X11/keysym.h>

using namespace CVD;

CVD::Exceptions::GLWindow::CreationError::CreationError(std::string w)
    : CVD::Exceptions::GLWindow::All { "GLWindow creation error: " + w }
{
}

CVD::Exceptions::GLWindow::RuntimeError::RuntimeError(std::string w)
    : CVD::Exceptions::GLWindow::All { "GLWindow error: " + w }
{
}

struct GLWindow::State
{
	ImageRef size;
	ImageRef position;
	std::string title;
	Display* display;
	Window window;
	Atom delete_atom;
	Cursor null_cursor;
	GLXContext context;
};

void CVD::GLWindow::init(const ImageRef& size, int bpp, const std::string& title, const std::string& disp)
{
	Display* display = XOpenDisplay(disp == "" ? NULL : const_cast<char*>(disp.c_str()));
	if(display == 0)
		throw Exceptions::GLWindow::CreationError("Cannot open X display");

	int visualAttributes[] = {
		GLX_RGBA,
		GLX_DOUBLEBUFFER,
		GLX_RED_SIZE, bpp / 3,
		GLX_GREEN_SIZE, bpp / 3,
		GLX_BLUE_SIZE, bpp / 3,
		GLX_DEPTH_SIZE, 8,
		GLX_STENCIL_SIZE, 8,
		None
	};
	XVisualInfo* visualInfo = glXChooseVisual(display, DefaultScreen(display), visualAttributes);
	if(visualInfo == 0)
	{
		XCloseDisplay(display);
		throw Exceptions::GLWindow::CreationError("glXChooseVisual failed");
	}

	Window rootWindow = RootWindow(display, visualInfo->screen);
	XWindowAttributes windowAttributes;

	XGetWindowAttributes(display, rootWindow, &windowAttributes);

	XSetWindowAttributes attributes;
	attributes.border_pixel = 0;
	attributes.colormap = XCreateColormap(display, rootWindow, visualInfo->visual, AllocNone);
	attributes.event_mask = KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | StructureNotifyMask | ExposureMask;

	Window window = XCreateWindow(display,
	    rootWindow,
	    0, 0, size.x, size.y,
	    0, visualInfo->depth,
	    InputOutput,
	    visualInfo->visual,
	    CWBorderPixel | CWColormap | CWEventMask,
	    &attributes);
	XStoreName(display, window, title.c_str());
	XClassHint classHint;
	char res_name[] = "cvd";
	classHint.res_class = res_name;
	classHint.res_name = (char*)title.c_str();
	XSetClassHint(display, window, &classHint);
	XMapWindow(display, window);
	XEvent ev;
	do
	{
		XNextEvent(display, &ev);
	} while(ev.type != MapNotify);

	Atom delete_atom = XInternAtom(display, "WM_DELETE_WINDOW", True);
	XSetWMProtocols(display, window, &delete_atom, 1);

	GLXContext context = glXCreateContext(display, visualInfo, 0, True);
	if(context == 0)
	{
		XDestroyWindow(display, window);
		XCloseDisplay(display);
		throw Exceptions::GLWindow::CreationError("glXCreateContext failed");
	}

	if(glXMakeCurrent(display, window, context) == False)
	{
		glXDestroyContext(display, context);
		XDestroyWindow(display, window);
		XCloseDisplay(display);
		throw Exceptions::GLWindow::CreationError("glXMakeCurrent failed");
	}
	glLoadIdentity();
	glViewport(0, 0, size.x, size.y);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glColor3f(1.0f, 1.0f, 1.0f);
	glRasterPos2f(-1, 1);
	glOrtho(-0.375, size.x - 0.375, size.y - 0.375, -0.375, -1, 1); //offsets to make (0,0) the top left pixel (rather than off the display)
	glPixelZoom(1, -1);

	XColor black = { 0, 0, 0, 0, 0, 0 };
	XFontStruct* fixed = XLoadQueryFont(display, "-misc-fixed-medium-r-*-*-12-*-*-*-*-*-*-1");
	Cursor null_cursor = XCreateGlyphCursor(display, fixed->fid, fixed->fid, ' ', ' ', &black, &black);
	XFreeFont(display, fixed);

	state = new State();
	state->size = size;
	state->title = title;
	state->display = display;
	state->window = window;
	state->delete_atom = delete_atom;
	state->null_cursor = null_cursor;
	state->context = context;
}

CVD::GLWindow::~GLWindow()
{
	glXMakeCurrent(state->display, None, 0);
	glXDestroyContext(state->display, state->context);

	XUnmapWindow(state->display, state->window);
	XDestroyWindow(state->display, state->window);
	XCloseDisplay(state->display);
	delete state;
}

ImageRef CVD::GLWindow::size() const { return state->size; }

void CVD::GLWindow::set_size(const ImageRef& s_)
{
	// we don't set state->size here, so that it changes through the event system
	// and we react to it there
	XResizeWindow(state->display, state->window, s_.x, s_.y);
}

ImageRef CVD::GLWindow::position() const { return state->position; }

void CVD::GLWindow::set_position(const ImageRef& p_)
{
	state->position = p_;
	XMoveWindow(state->display, state->window, p_.x, p_.y);
	XFlush(state->display);
}

void CVD::GLWindow::set_cursor_position(const ImageRef& where)
{
	XWarpPointer(state->display, None, state->window, 0, 0, 0, 0, where.x, where.y);
}

ImageRef CVD::GLWindow::cursor_position() const
{
	Window wtmp;
	int itmp;
	unsigned int utmp;
	ImageRef where;
	XQueryPointer(state->display, state->window, &wtmp, &wtmp, &itmp, &itmp, &where.x, &where.y, &utmp);
	return where;
}

void CVD::GLWindow::show_cursor(bool show)
{
	if(show)
		XUndefineCursor(state->display, state->window);
	else
		XDefineCursor(state->display, state->window, state->null_cursor);
}

std::string CVD::GLWindow::title() const
{
	return state->title;
}

void CVD::GLWindow::set_title(const std::string& title)
{
	state->title = title;
	XStoreName(state->display, state->window, title.c_str());
}

void CVD::GLWindow::swap_buffers()
{
	glXSwapBuffers(state->display, state->window);
}

inline int convertButton(unsigned int button)
{
	switch(button)
	{
		case Button1:
			return GLWindow::BUTTON_LEFT;
		case Button2:
			return GLWindow::BUTTON_MIDDLE;
		case Button3:
			return GLWindow::BUTTON_RIGHT;
		case Button4:
			return GLWindow::BUTTON_WHEEL_UP;
		case Button5:
			return GLWindow::BUTTON_WHEEL_DOWN;
	}
	return 0;
}

inline int convertButtonState(unsigned int state)
{
	int ret = 0;
	if(state & Button1Mask)
		ret |= GLWindow::BUTTON_LEFT;
	if(state & Button2Mask)
		ret |= GLWindow::BUTTON_MIDDLE;
	if(state & Button3Mask)
		ret |= GLWindow::BUTTON_RIGHT;
	if(state & ControlMask)
		ret |= GLWindow::BUTTON_MOD_CTRL;
	if(state & ShiftMask)
		ret |= GLWindow::BUTTON_MOD_SHIFT;
	return ret;
}

void CVD::GLWindow::handle_events(EventHandler& handler)
{
	XEvent event;
	KeySym k;
	while(XPending(state->display))
	{
		XNextEvent(state->display, &event);
		switch(event.type)
		{
			case ButtonPress:
				handler.on_mouse_down(*this, ImageRef(event.xbutton.x, event.xbutton.y),
				    convertButtonState(event.xbutton.state), convertButton(event.xbutton.button));
				break;
			case ButtonRelease:
				handler.on_mouse_up(*this, ImageRef(event.xbutton.x, event.xbutton.y),
				    convertButtonState(event.xbutton.state), convertButton(event.xbutton.button));
				break;
			case MotionNotify:
				handler.on_mouse_move(*this, ImageRef(event.xmotion.x, event.xmotion.y), convertButtonState(event.xbutton.state));
				break;
			case KeyPress:
			{
				XLookupString(&event.xkey, 0, 0, &k, 0);
				handler.on_key_down(*this, static_cast<unsigned int>(k)); //KeySyms are 29 bits only
				break;
			}
			case KeyRelease:
				XLookupString(&event.xkey, 0, 0, &k, 0);
				handler.on_key_up(*this, static_cast<unsigned int>(k));
				break;
				//case UnmapNotify: active = 0; break;
				//case MapNotify: active = 1; break;
			case ConfigureNotify:
				if(event.xconfigure.width != state->size.x || event.xconfigure.height != state->size.y)
				{
					activate();
					state->size = ImageRef(event.xconfigure.width, event.xconfigure.height);
					glViewport(0, 0, state->size.x, state->size.y);
					//glRasterPos2f(0,0);
					//glPixelZoom(float(event.xconfigure.width)/myWidth,-float(event.xconfigure.height)/myHeight);
					handler.on_resize(*this, state->size);
				}
				break;
			case Expose:
				handler.on_event(*this, EVENT_EXPOSE);
				break;
			case ClientMessage:
				if(event.xclient.data.l[0] == (int)state->delete_atom)
					handler.on_event(*this, EVENT_CLOSE);
				else
					handler.on_event(*this, static_cast<unsigned int>(event.xclient.message_type));
				break;
			default:
				handler.on_event(*this, event.type);
				break;
		}
	}
}

class SaveEvents : public GLWindow::EventHandler
{
	private:
	std::vector<GLWindow::Event>& events;

	public:
	SaveEvents(std::vector<GLWindow::Event>& events_)
	    : events(events_)
	{
	}
	void on_key_down(GLWindow&, unsigned int key) override
	{
		GLWindow::Event e;
		e.type = GLWindow::Event::KEY_DOWN;
		e.which = key;
		events.push_back(e);
	}
	void on_key_up(GLWindow&, unsigned int key) override
	{
		GLWindow::Event e;
		e.type = GLWindow::Event::KEY_UP;
		e.which = key;
		events.push_back(e);
	}

	void on_mouse_move(GLWindow&, ImageRef where, unsigned int state) override
	{
		GLWindow::Event e;
		e.type = GLWindow::Event::MOUSE_MOVE;
		e.state = state;
		e.where = where;
		events.push_back(e);
	}

	void on_mouse_down(GLWindow&, ImageRef where, unsigned int state, unsigned int button) override
	{
		GLWindow::Event e;
		e.type = GLWindow::Event::MOUSE_DOWN;
		e.state = state;
		e.which = button;
		e.where = where;
		events.push_back(e);
	}

	void on_mouse_up(GLWindow&, ImageRef where, unsigned int state, unsigned int button) override
	{
		GLWindow::Event e;
		e.type = GLWindow::Event::MOUSE_UP;
		e.state = state;
		e.which = button;
		e.where = where;
		events.push_back(e);
	}

	void on_resize(GLWindow&, ImageRef size) override
	{
		GLWindow::Event e;
		e.type = GLWindow::Event::RESIZE;
		e.size = size;
		events.push_back(e);
	}

	void on_event(GLWindow&, unsigned int event) override
	{
		GLWindow::Event e;
		e.type = GLWindow::Event::EVENT;
		e.which = event;
		events.push_back(e);
	}
};

void CVD::GLWindow::get_events(std::vector<Event>& events)
{
	SaveEvents saver(events);
	handle_events(saver);
}

bool CVD::GLWindow::EventSummary::should_quit() const
{
	return key_down.count(XK_Escape) || events.count(GLWindow::EVENT_CLOSE);
}

class MakeSummary : public GLWindow::EventHandler
{
	private:
	GLWindow::EventSummary& summary;

	public:
	MakeSummary(GLWindow::EventSummary& summary_)
	    : summary(summary_)
	{
	}

	void on_key_down(GLWindow&, unsigned int key) override { ++summary.key_down[key]; }
	void on_key_up(GLWindow&, unsigned int key) override { ++summary.key_up[key]; }
	void on_mouse_move(GLWindow&, ImageRef where, unsigned int) override
	{
		summary.cursor = where;
		summary.cursor_moved = true;
	}
	void on_mouse_down(GLWindow&, ImageRef where, unsigned int state, unsigned int button) override { summary.mouse_down[button] = std::make_pair(where, state); }
	void on_mouse_up(GLWindow&, ImageRef where, unsigned int state, unsigned int button) override { summary.mouse_up[button] = std::make_pair(where, state); }
	void on_event(GLWindow&, unsigned int event) override { ++summary.events[event]; }
};

void GLWindow::get_events(EventSummary& summary)
{
	summary.cursor = cursor_position();
	MakeSummary ms(summary);
	handle_events(ms);
}

bool CVD::GLWindow::has_events() const
{
	return XPending(state->display);
}

void CVD::GLWindow::activate()
{
	if(glXMakeCurrent(state->display, state->window, state->context) == False)
		throw Exceptions::GLWindow::RuntimeError("glXMakeCurrent failed");
}
