/*
 * This file is subject to the terms of the GFX License. If a copy of
 * the license was not distributed with this file, you can obtain one at:
 *
 *              http://ugfx.io/license.html
 */

// We need to include stdio.h below. Turn off GFILE_NEED_STDIO just for this file to prevent conflicts
#define GFILE_NEED_STDIO_MUST_BE_OFF

#include "gfx.h"

#if GFX_USE_GDISP

#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xresource.h>
#include <stdio.h>
#include <stdlib.h>

#define GDISP_DRIVER_VMT				GDISPVMT_X11
#include "gdisp_lld_config.h"
#include "../../../src/gdisp/gdisp_driver.h"

// Configuration parameters for this driver
#ifndef GDISP_FORCE_24BIT
	#define GDISP_FORCE_24BIT			GFXOFF
#endif
#ifndef GDISP_SCREEN_WIDTH
	#define GDISP_SCREEN_WIDTH			640
#endif
#ifndef GDISP_SCREEN_HEIGHT
	#define GDISP_SCREEN_HEIGHT			480
#endif
#ifndef GKEYBOARD_X_NO_LAYOUT
	/**
	 * Setting this to GFXON turns off the layout engine.
	 * In this situation "cooked" characters are returned but
	 * shift states etc are lost.
	 * As only a limited number of keyboard layouts are currently
	 * defined for X in uGFX (currently none), setting this
	 * to GFXON enables the X keyboard mapping to be pass non-English
	 * characters to uGFX or to handle non-standard keyboard layouts at
	 * the expense of losing special function keys etc.
	 */
	// We set this to GFXON by default as currently the X layout code is not complete!
	#define GKEYBOARD_X_NO_LAYOUT		GFXON
#endif
#ifndef GKEYBOARD_X_DEFAULT_LAYOUT
	#define GKEYBOARD_X_DEFAULT_LAYOUT	KeyboardLayout_X_US
#endif

// Driver status flags
#define GDISP_FLG_READY				(GDISP_FLG_DRIVER<<0)

#if GINPUT_NEED_MOUSE
	// Include mouse support code
	#define GMOUSE_DRIVER_VMT		GMOUSEVMT_X11
	#include "../../../src/ginput/ginput_driver_mouse.h"

	// Forward definitions
	static gBool XMouseInit(GMouse *m, unsigned driverinstance);
	static gBool XMouseRead(GMouse *m, GMouseReading *prd);

	const GMouseVMT const GMOUSE_DRIVER_VMT[1] = {{
		{
			GDRIVER_TYPE_MOUSE,
			GMOUSE_VFLG_NOPOLL|GMOUSE_VFLG_DYNAMICONLY,
				// Extra flags for testing only
				//GMOUSE_VFLG_TOUCH|GMOUSE_VFLG_SELFROTATION|GMOUSE_VFLG_DEFAULTFINGER
				//GMOUSE_VFLG_CALIBRATE|GMOUSE_VFLG_CAL_EXTREMES|GMOUSE_VFLG_CAL_TEST|GMOUSE_VFLG_CAL_LOADFREE
				//GMOUSE_VFLG_ONLY_DOWN|GMOUSE_VFLG_POORUPDOWN
			sizeof(GMouse),
			_gmouseInitDriver, _gmousePostInitDriver, _gmouseDeInitDriver
		},
		1,				// z_max
		0,				// z_min
		1,				// z_touchon
		0,				// z_touchoff
		{				// pen_jitter
			0,				// calibrate
			0,				// click
			0				// move
		},
		{				// finger_jitter
			0,				// calibrate
			2,				// click
			2				// move
		},
		XMouseInit,		// init
		0,				// deinit
		XMouseRead,		// get
		0,				// calsave
		0				// calload
	}};
#endif

#if GINPUT_NEED_KEYBOARD
	// Include mouse support code
	#define GKEYBOARD_DRIVER_VMT		GKEYBOARDVMT_X
	#include "../../../src/ginput/ginput_driver_keyboard.h"

	#if !GKEYBOARD_X_NO_LAYOUT
		#if GKEYBOARD_LAYOUT_OFF
			#error "The X keyboard driver is using the layout engine. Please set GKEYBOARD_LAYOUT_OFF=GFXOFF or GKEYBOARD_X_NO_LAYOUT=GFXON."
		#endif

		// Forward definitions
		extern gU8	GKEYBOARD_X_DEFAULT_LAYOUT[];

		#include "../../../src/ginput/ginput_keyboard_microcode.h"
		#include <X11/keysym.h>

		// This is the layout code for the English US keyboard.
		//	We make it public so that a user can switch to a different layout if required.
		gU8	KeyboardLayout_X_US[] = {
			KMC_HEADERSTART, KMC_HEADER_ID1, KMC_HEADER_ID2, KMC_HEADER_VER_1,

			// TODO
			#error "The code to do keyboard layouts in X is not complete."

			// Transient Shifters: SHIFT, CTRL, ALT, WINKEY
			// Locking Shifters: CAPSLOCK, NUMLOCK and SCROLLLOCK
			// Keyup, Repeat
			// 0 - 9
			// A - Z
			// Number pad
			// Symbols
			// Special Keys
			// Anything else
			// EOF
			KMC_RECORDSTART, 0
		};
	#elif !GKEYBOARD_LAYOUT_OFF
		#if GFX_COMPILER_WARNING_TYPE == GFX_COMPILER_WARNING_DIRECT
			#warning "The X keyboard driver is not using the layout engine. If no other keyboard is using it consider defining GKEYBOARD_LAYOUT_OFF=GFXON to save code size."
		#elif GFX_COMPILER_WARNING_TYPE == GFX_COMPILER_WARNING_MACRO
			COMPILER_WARNING("The X keyboard driver is not using the layout engine. If no other keyboard is using it consider defining GKEYBOARD_LAYOUT_OFF=GFXON to save code size.")
		#endif
	#endif

	// Forward definitions
	static gBool XKeyboardInit(GKeyboard *k, unsigned driverinstance);
	static int XKeyboardGetData(GKeyboard *k, gU8 *pch, int sz);

	const GKeyboardVMT const GKEYBOARD_DRIVER_VMT[1] = {{
		{
			GDRIVER_TYPE_KEYBOARD,
			GKEYBOARD_VFLG_NOPOLL,			//  GKEYBOARD_VFLG_DYNAMICONLY
			sizeof(GKeyboard),
			_gkeyboardInitDriver, _gkeyboardPostInitDriver, _gkeyboardDeInitDriver
		},
	 	// The default keyboard layout
		#if GKEYBOARD_X_NO_LAYOUT
			0,
		#else
			GKEYBOARD_X_DEFAULT_LAYOUT,
		#endif
		XKeyboardInit,			// init
		0,						// deinit
		XKeyboardGetData,		// getdata
		0						// putdata		void	(*putdata)(GKeyboard *k, char ch);		Optional
	}};

	static int			keypos;
	static gU8		keybuffer[8];
	static GKeyboard	*keyboard;
#endif

static gBool			initdone;
static Display			*dis;
static int				scr;
static XEvent			evt;
static Colormap			cmap;
static XVisualInfo		vis;
static XContext			cxt;
static Atom				wmDelete;

typedef struct xPriv {
	Pixmap			pix;
	GC 				gc;
	Window			win;
	#if GINPUT_NEED_MOUSE
		gCoord		mousex, mousey;
		gU16	buttons;
		GMouse *	mouse;
	#endif
} xPriv;

static void ProcessEvent(GDisplay *g, xPriv *priv) {
	switch(evt.type) {
	case MapNotify:
		XSelectInput(dis, evt.xmap.window,
				StructureNotifyMask|ExposureMask|ButtonPressMask|ButtonReleaseMask|PointerMotionMask|KeyPressMask|KeyReleaseMask|KeymapStateMask);
		g->flags |= GDISP_FLG_READY;
		break;
	case UnmapNotify:
		XCloseDisplay(dis);
		exit(0);
		break;
	case ClientMessage:
		if ((Atom)evt.xclient.data.l[0] == wmDelete) {
			XCloseDisplay(dis);
			exit(0);
		}
		break;
	case Expose:
		XCopyArea(dis, priv->pix, evt.xexpose.window, priv->gc,
			evt.xexpose.x, evt.xexpose.y,
			evt.xexpose.width, evt.xexpose.height,
			evt.xexpose.x, evt.xexpose.y);
		break;
	#if GINPUT_NEED_MOUSE
		case ButtonPress:
			priv->mousex = evt.xbutton.x;
			priv->mousey = evt.xbutton.y;
			switch(evt.xbutton.button){
			case 1:	priv->buttons |= GINPUT_MOUSE_BTN_LEFT;		break;
			case 2:	priv->buttons |= GINPUT_MOUSE_BTN_MIDDLE;	break;
			case 3:	priv->buttons |= GINPUT_MOUSE_BTN_RIGHT;	break;
			case 4:	priv->buttons |= GINPUT_MOUSE_BTN_4;		break;
			}
			_gmouseWakeup(priv->mouse);
			break;
		case ButtonRelease:
			priv->mousex = evt.xbutton.x;
			priv->mousey = evt.xbutton.y;
			switch(evt.xbutton.button){
			case 1:	priv->buttons &= ~GINPUT_MOUSE_BTN_LEFT;	break;
			case 2:	priv->buttons &= ~GINPUT_MOUSE_BTN_MIDDLE;	break;
			case 3:	priv->buttons &= ~GINPUT_MOUSE_BTN_RIGHT;	break;
			case 4:	priv->buttons &= ~GINPUT_MOUSE_BTN_4;		break;
			}
			_gmouseWakeup(priv->mouse);
			break;
		case MotionNotify:
			priv->mousex = evt.xmotion.x;
			priv->mousey = evt.xmotion.y;
			_gmouseWakeup(priv->mouse);
			break;
	#endif
	#if GINPUT_NEED_KEYBOARD
		case KeymapNotify:
			XRefreshKeyboardMapping(&evt.xmapping);
			break;
		case KeyPress:
			#if !GKEYBOARD_X_NO_LAYOUT
				// TODO
				#error "The code to do keyboard layouts in X is not complete."
			#endif
			 /* ignore these when there is no layout engine */
			break;
		case KeyRelease:
			#if !GKEYBOARD_X_NO_LAYOUT
				// TODO
				#error "The code to do keyboard layouts in X is not complete."
			#endif
			if (keyboard && !keyboard->pLayout && keypos < (int)sizeof(keybuffer)) {
				int		len;

				len = XLookupString(&evt.xkey, (char *)(keybuffer+keypos), sizeof(keybuffer)-keypos, /*&keysym*/0, NULL);
				if (len > 0) {
					keypos += len;
					_gkeyboardWakeup(keyboard);
				}
			}
			break;
	#endif
	}
}

/* this is the X11 thread which keeps track of all events */
static GFX_THREAD_STACK(waXThread, 1024);
static GFX_THREAD_FUNCTION(ThreadX, arg) {
	GDisplay	*g;
	(void)arg;

	while(1) {
		gfxSleepMilliseconds(100);
		while(XPending(dis)) {
			XNextEvent(dis, &evt);
			XFindContext(evt.xany.display, evt.xany.window, cxt, (XPointer*)&g);
			ProcessEvent(g, (xPriv *)g->priv);
		}
	}
	return 0;
}

static int FatalXIOError(Display *d) {
	(void) d;

	/* The window has closed */
	fprintf(stderr, "GFX Window closed!\n");
	exit(0);
}

LLDSPEC gBool gdisp_lld_init(GDisplay *g) {
	XSizeHints				*pSH;
	XSetWindowAttributes	xa;
	XTextProperty			WindowTitle;
	char *					WindowTitleText;
	xPriv					*priv;

	if (!initdone) {
		gThread			hth;

		initdone = gTrue;
		#if GFX_USE_OS_LINUX || GFX_USE_OS_OSX
			XInitThreads();
		#endif

		dis = XOpenDisplay(0);
		scr = DefaultScreen(dis);
		cxt = XUniqueContext();
		wmDelete = XInternAtom(dis, "WM_DELETE_WINDOW", False);
		XSetIOErrorHandler(FatalXIOError);

		#if GDISP_FORCE_24BIT
			if (!XMatchVisualInfo(dis, scr, 24, TrueColor, &vis)) {
				fprintf(stderr, "Your display has no TrueColor mode\n");
				XCloseDisplay(dis);
				return gFalse;
			}
			cmap = XCreateColormap(dis, RootWindow(dis, scr),
					vis.visual, AllocNone);
		#else
			vis.visual = CopyFromParent;
			vis.depth = DefaultDepth(dis, scr);
			cmap = DefaultColormap(dis, scr);
		#endif
		fprintf(stderr, "Running GFX Window in %d bit color\n", vis.depth);

		if (!(hth = gfxThreadCreate(waXThread, sizeof(waXThread), gThreadpriorityHigh, ThreadX, 0))) {
			fprintf(stderr, "Cannot start X Thread\n");
			XCloseDisplay(dis);
			exit(0);
		}
		#if GFX_USE_OS_LINUX || GFX_USE_OS_OSX
			pthread_detach(hth);
		#endif
		gfxThreadClose(hth);
	}

	g->priv = gfxAlloc(sizeof(xPriv));
	priv = (xPriv *)g->priv;
	g->board = 0;					// No board interface for this driver

	xa.colormap = cmap;
	xa.border_pixel = 0xFFFFFF;
	xa.background_pixel = 0x000000;

	priv->win = XCreateWindow(dis, RootWindow(dis, scr), 16, 16,
			GDISP_SCREEN_WIDTH, GDISP_SCREEN_HEIGHT,
			0, vis.depth, InputOutput, vis.visual,
			CWBackPixel|CWColormap|CWBorderPixel, &xa);
	XSync(dis, TRUE);

	XSaveContext(dis, priv->win, cxt, (XPointer)g);
	XSetWMProtocols(dis, priv->win, &wmDelete, 1);

	{
		char					buf[132];
		sprintf(buf, "uGFX - %u", g->systemdisplay+1);
		WindowTitleText = buf;
		XStringListToTextProperty(&WindowTitleText, 1, &WindowTitle);
		XSetWMName(dis, priv->win, &WindowTitle);
		XSetWMIconName(dis, priv->win, &WindowTitle);
		XSync(dis, TRUE);
	}

	pSH = XAllocSizeHints();
	pSH->flags = PSize | PMinSize | PMaxSize;
	pSH->min_width = pSH->max_width = pSH->base_width = GDISP_SCREEN_WIDTH;
	pSH->min_height = pSH->max_height = pSH->base_height = GDISP_SCREEN_HEIGHT;
	XSetWMNormalHints(dis, priv->win, pSH);
	XFree(pSH);
	XSync(dis, TRUE);

	priv->pix = XCreatePixmap(dis, priv->win,
				GDISP_SCREEN_WIDTH, GDISP_SCREEN_HEIGHT, vis.depth);
	XSync(dis, TRUE);

	priv->gc = XCreateGC(dis, priv->win, 0, 0);
	XSetBackground(dis, priv->gc, BlackPixel(dis, scr));
	XSync(dis, TRUE);

	// Create the associated mouse before the map
	#if GINPUT_NEED_MOUSE
		priv->mouse = (GMouse *)gdriverRegister((const GDriverVMT const *)GMOUSE_DRIVER_VMT, g);
	#endif

	XSelectInput(dis, priv->win, StructureNotifyMask);
	XMapWindow(dis, priv->win);

	// Wait for the window creation to complete (for safety)
	while(!(((volatile GDisplay *)g)->flags & GDISP_FLG_READY))
		gfxSleepMilliseconds(100);

	/* Initialise the GDISP structure to match */
    g->g.Orientation = gOrientation0;
    g->g.Powermode = gPowerOn;
    g->g.Backlight = 100;
    g->g.Contrast = 50;
    g->g.Width = GDISP_SCREEN_WIDTH;
    g->g.Height = GDISP_SCREEN_HEIGHT;

    return gTrue;
}

LLDSPEC void gdisp_lld_draw_pixel(GDisplay *g)
{
	xPriv *	priv = (xPriv *)g->priv;
	XColor	col;

	col.red = RED_OF(g->p.color) << 8;
	col.green = GREEN_OF(g->p.color) << 8;
	col.blue = BLUE_OF(g->p.color) << 8;
	XAllocColor(dis, cmap, &col);
	XSetForeground(dis, priv->gc, col.pixel);
	XDrawPoint(dis, priv->pix, priv->gc, (int)g->p.x, (int)g->p.y );
	XDrawPoint(dis, priv->win, priv->gc, (int)g->p.x, (int)g->p.y );
	XFlush(dis);
}

#if GDISP_HARDWARE_FILLS
	LLDSPEC void gdisp_lld_fill_area(GDisplay *g) {
		xPriv *	priv = (xPriv *)g->priv;
		XColor	col;

		col.red = RED_OF(g->p.color) << 8;
		col.green = GREEN_OF(g->p.color) << 8;
		col.blue = BLUE_OF(g->p.color) << 8;
		XAllocColor(dis, cmap, &col);
		XSetForeground(dis, priv->gc, col.pixel);
		XFillRectangle(dis, priv->pix, priv->gc, g->p.x, g->p.y, g->p.cx, g->p.cy);
		XFillRectangle(dis, priv->win, priv->gc, g->p.x, g->p.y, g->p.cx, g->p.cy);
		XFlush(dis);
	}
#endif

#if 0 && GDISP_HARDWARE_BITFILLS
	LLDSPEC void gdisp_lld_blit_area(GDisplay *g) {
		// Start of Bitblit code

		//XImage			bitmap;
		//gPixel			*bits;
		//	bits = malloc(vis.depth * GDISP_SCREEN_WIDTH * GDISP_SCREEN_HEIGHT);
		//	bitmap = XCreateImage(dis, vis, vis.depth, ZPixmap,
		//				0, bits, GDISP_SCREEN_WIDTH, GDISP_SCREEN_HEIGHT,
		//				0, 0);
	}
#endif

#if GDISP_HARDWARE_PIXELREAD
	LLDSPEC	gColor gdisp_lld_get_pixel_color(GDisplay *g) {
		xPriv *	priv = (xPriv *)g->priv;
		XColor	color;
		XImage *img;

		img = XGetImage (dis, priv->pix, g->p.x, g->p.y, 1, 1, AllPlanes, XYPixmap);
		color.pixel = XGetPixel (img, 0, 0);
		XFree(img);
		XQueryColor(dis, cmap, &color);
		return RGB2COLOR(color.red>>8, color.green>>8, color.blue>>8);
	}
#endif

#if GDISP_NEED_SCROLL && GDISP_HARDWARE_SCROLL
	LLDSPEC void gdisp_lld_vertical_scroll(GDisplay *g) {
		xPriv *	priv = (xPriv *)g->priv;

		if (g->p.y1 > 0) {
			XCopyArea(dis, priv->pix, priv->pix, priv->gc, g->p.x, g->p.y+g->p.y1, g->p.cx, g->p.cy-g->p.y1, g->p.x, g->p.y);
			XCopyArea(dis, priv->pix, priv->win, priv->gc, g->p.x, g->p.y, g->p.cx, g->p.cy-g->p.y1, g->p.x, g->p.y);
		} else {
			XCopyArea(dis, priv->pix, priv->pix, priv->gc, g->p.x, g->p.y, g->p.cx, g->p.cy+g->p.y1, g->p.x, g->p.y-g->p.y1);
			XCopyArea(dis, priv->pix, priv->win, priv->gc, g->p.x, g->p.y-g->p.y1, g->p.cx, g->p.cy+g->p.y1, g->p.x, g->p.y-g->p.y1);
		}
	}
#endif

#if GINPUT_NEED_MOUSE
	static gBool XMouseInit(GMouse *m, unsigned driverinstance) {
		(void)	m;
		(void)	driverinstance;
		return gTrue;
	}
	static gBool XMouseRead(GMouse *m, GMouseReading *pt) {
		xPriv	*	priv;

		priv = m->display->priv;
		pt->x = priv->mousex;
		pt->y = priv->mousey;
		pt->z = (priv->buttons & GINPUT_MOUSE_BTN_LEFT) ? 1 : 0;
		pt->buttons = priv->buttons;
		return gTrue;
	}
#endif /* GINPUT_NEED_MOUSE */

#if GINPUT_NEED_KEYBOARD
	static gBool XKeyboardInit(GKeyboard *k, unsigned driverinstance) {
		(void)	driverinstance;

		// Only one please
		if (keyboard)
			return gFalse;

		keyboard = k;
		return gTrue;
	}

	static int XKeyboardGetData(GKeyboard *k, gU8 *pch, int sz) {
		int		i, j;
		(void)	k;

		if (!keypos)
			return 0;

		for(i = 0; i < keypos && i < sz; i++)
			pch[i] = keybuffer[i];
		keypos -= i;
		for(j=0; j < keypos; j++)
			keybuffer[j] = keybuffer[i+j];
		return i;
	}
#endif /* GINPUT_NEED_KEYBOARD */

#endif /* GFX_USE_GDISP */