/*	XGAppCore_xwin.cpp
 *
 *		X specific implementation of various XGAppCore methods
 */

/**********************************************************************

  YAAF is Copyright 1997-1999 by William Woody.

  Redistribution and use in source and binary forms, with or without 
  modification, are permitted provided that the following conditions 
  are met:

  Redistributions of source code must retain the above copyright notice, 
  this list of conditions and the following disclaimer.

  Neither the name of the copyright holder or other contributors may
  be used to promote products derived from this software without
  specific written prior permission.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 
  BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 
  ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 
  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
  SOFTWARE.
  
  Contact:   woody@alumni.caltech.edu
  Web page:  http://www.pandawave.com/yaaf/

**********************************************************************/

#include <XConfig.h>

#if OPT_XWIN

#include <XApplication.h>
#include <XDraw.h>
#include <XWindow.h>
#include <XMessages.h>
#include <XMenu.h>
#include <XInline.h>
#include <XError.h>
#include <XModifiers.h>
#include <XDataUtil.h>
#include <XFile.h>

#include <time.h>
#include <sys/times.h>

/************************************************************************/
/*																		*/
/*	Miscellaneous Globals												*/
/*																		*/
/************************************************************************/

extern int					_GArgC;
extern char					**_GArgV;

static XGView				*GCapture;
Atom						_GWMProtocols;
Atom						_GDelWindowAtom;
extern Display				*_GDisplay;
Window						_GEventWindow;			// for cursor util

short						GMenuCommand;			// menu command

XEvent						XGAppCore::gLastMessage;

/************************************************************************/
/*																		*/
/*	Support																*/
/*																		*/
/************************************************************************/

/*	GetDisplayArgument
 *
 *		Scan the argc/argv list for the -display command argument
 */

char *GetDisplayArgument()
{
	int i;
	
	for (i = 1; i < _GArgC-1; i++) {
		if (!strcmp(_GArgV[i],"-display")) {
			return _GArgV[i+1];
		}
	}
	return NULL;
}

/************************************************************************/
/*																		*/
/*	Standard X Error Handler											*/
/*																		*/
/************************************************************************/

/*	YAAFErrorHandler
 *
 *		This is my error handler; this gives me a chance to figure
 *	out what went wrong. If debug is not set, we simply ignore
 *	these errors, as YAAF is relatively robust and can survive
 *	the occassional misfire.
 */

static int YAAFErrorHandler(Display *disp, XErrorEvent *err)
{
#if DEBUG
	char errtext[256];
	
	XGetErrorText(disp,err->error_code,errtext,sizeof(errtext));
	printf("Warning: An X protocol error occured in YAAF\n");
	printf("         Error \"%s\"\n",errtext);
#endif
}

/************************************************************************/
/*																		*/
/*	OS Init/Shutdown													*/
/*																		*/
/************************************************************************/

#pragma mark Initialization and Shutdown

/*	XGAppCore::InitOS
 *
 *		Initialize the OS. I don't need to do very much here...
 */

void XGAppCore::InitOS()
{
	/*
	 *	Initialize my connection to the X server through Xlib
	 */
	 
	_GDisplay = XOpenDisplay(GetDisplayArgument());
	if (_GDisplay == NULL) {
		/*
		 *	Server not up -- punt immediately, no questions asked.
		 */
		
		printf("Unable to connect to X server\n");
		throw XGAbort();			// Throw no-warning abort error
	}
	
	XSetErrorHandler(YAAFErrorHandler);
#if DEBUG
	XSynchronize(_GDisplay,true);	// debugging
#endif

	/*
	 *	Create my atoms
	 */
	
	_GDelWindowAtom = XInternAtom(_GDisplay,"WM_DELETE_WINDOW",False);
	_GWMProtocols = XInternAtom(_GDisplay,"WM_PROTOCOLS",False);
}

/*	XGAppCore::KillOS
 *
 *		Kill the OS. I don't need to do much here, either...
 */

void XGAppCore::KillOS()
{
	/*
	 *	Shut down my connection
	 */
	
	if (_GDisplay) XCloseDisplay(_GDisplay);
}

/*	XGAppCore::GetAppArgs
 *
 *		Called right before the event pump is run, this parses
 *	the arguments on the command line and passes them to my app
 */

void XGAppCore::GetAppArgs()
{
	int i;
	int count;
	char *n;
	XGFileSpecifier fspec;
	
	/*
	 *	How many files specified?
	 */
	
	count = 0;
	for (i = 1; i < _GArgC; i++) {
		n = _GArgV[i];
		if (*n == '-') i++;
		else count++;
	}
	
	/*
	 *	Send messages around my system
	 */
	
	if (count == 0) {
		XGDispatch::SendDispatch(KXGOpenApplication,0,NULL);
	} else {
		for (i = 1; i < _GArgC; i++) {
			n = _GArgV[i];
			if (*n == '-') i++;
			else {
				fspec = _GArgV[i];
				XGDispatch::SendDispatch(KXGDoOpenDocument,0,(void *)&fspec);
			}
		}
	}
	return NULL;
}

/************************************************************************/
/*																		*/
/*	Window Order Support												*/
/*																		*/
/************************************************************************/

/*	GetParentWindow
 *
 *		Given a pointer to a window, this either returns the parent of the
 *	window or None if the parent is the root
 */

Window XGAppCore::GetParentWindow(Window w)
{
	unsigned int nchildren;
	Window *children;
	Window parent;
	Window root;

	::XQueryTree(_GDisplay,
				 w,
				 &root,
				 &parent,
				 &children,
				 &nchildren);

	::XFree((void *)children);
	if (parent == DefaultRootWindow(_GDisplay)) return None;
	else return parent;
}	

/*	ArrangeOrder
 *
 *		Arrange the order of the windows associated with my application.
 *	This reaches out and makes sure my current modal dialog is in front of
 *	my modeless dialogs and document windows. This is frigheningly 
 *	antisocial, but there it is.
 */

void XGAppCore::ArrangeOrder(void)
{
	unsigned int nchildren;
	Window *children;
	Window parent;
	Window root;
	int i;
	XGWindow *wptr;
	unsigned int flag;

	if (fModalWindow == NULL) return;	/* Nothing to arrange... */
		
	/*
	 *	Get the children of the root window
	 */
	
	::XQueryTree(_GDisplay,
				 DefaultRootWindow(_GDisplay),
				 &root,
				 &parent,
				 &children,
				 &nchildren);
	
	/*
	 *	Scan the list for a modal window
	 */
	
	flag = 0;
	for (i = nchildren-1; i >= 0; i--) {
		wptr = XGWindow::GetXGWindowByParent(children[i]);
		if (wptr == NULL) continue;
		if (wptr == fModalWindow) {
			break;
		}
		flag = 1;
	}
	
	/*
	 *	Now see if we've a problem
	 */
	
	if ((i >= 0) && flag) {
		/*
		 *	We have a modal window not on top
		 */
		
		::XRaiseWindow(_GDisplay,children[i]);
	}
	
	::XFree((void *)children);
}

/*	GetSpecialKey
 *
 *		This determines if the keyboard event is a special key. If it is,
 *	this returns the special key code (YAAF key code)
 */

short XGAppCore::GetSpecialKey(KeySym sym)
{
	switch (sym) {
		case XK_KP_Up:
		case XK_Up:
			return KKeyUpCursor;
		case XK_KP_Down:
		case XK_Down:
			return KKeyDownCursor;
		case XK_KP_Left:
		case XK_Left:
			return KKeyLeftCursor;
		case XK_KP_Right:
		case XK_Right:
			return KKeyRightCursor;
		case XK_KP_Insert:
			return KKeyInsert;
		case XK_KP_Delete:
			return KKeyDelete;
		case XK_KP_Home:
			return KKeyHome;
		case XK_KP_End:
			return KKeyEnd;
		case XK_KP_Page_Up:
		case XK_Page_Up:
			return KKeyPageUp;
		case XK_KP_Page_Down:
		case XK_Page_Down:
			return KKeyPageDown;
		case XK_Escape:
			return KKeyEscape;
//		case XK_:
//			return KKeyHelp;
//		case XK_KP_:
//			return KKeyNumLock;
		case XK_Clear:
			return KKeyClear;
		case XK_F1:
			return KKeyF1;
		case XK_F2:
			return KKeyF2;
		case XK_F3:
			return KKeyF3;
		case XK_F4:
			return KKeyF4;
		case XK_F5:
			return KKeyF5;
		case XK_F6:
			return KKeyF6;
		case XK_F7:
			return KKeyF7;
		case XK_F8:
			return KKeyF8;
		case XK_F9:
			return KKeyF9;
		case XK_F10:
			return KKeyF10;
		case XK_F11:
			return KKeyF11;
		case XK_F12:
			return KKeyF12;
		case XK_F13:
			return KKeyF13;
		case XK_F14:
			return KKeyF14;
		case XK_F15:
			return KKeyF15;
		default:
			return 0;
	}
}

/*	DeriveFlags
 *
 *		Given an X-windows modifier flags, this converts to the YAAF
 *	modifier flags
 */

short XGAppCore::DeriveFlags(long f)
{
	short i;
	
	i = 0;
	if (f & ShiftMask)   i |= KKeyShift;
	if (f & LockMask)    i |= KKeyShift;		// treat caps lock like shift
	if (f & ControlMask) i |= KKeyControl;
	if (f & Mod1Mask)    i |= KKeyMod1;
	if (f & Mod2Mask)    i |= KKeyMod2;
	if (f & Mod3Mask)    i |= KKeyMod3;
	if (f & Mod4Mask)    i |= KKeyMod4;
	if (f & Mod5Mask)    i |= KKeyMod5;
	if (f & Button1Mask) i |= KKeyButton;
	if (f & Button2Mask) i |= KKeyButton2;
	if (f & Button3Mask) i |= KKeyButton3;
	if (f & Button4Mask) i |= KKeyButton4;
	if (f & Button5Mask) i |= KKeyButton5;
	gLastMods = i;
	
	return i;
}

/************************************************************************/
/*																		*/
/*	Event Processing													*/
/*																		*/
/************************************************************************/

/*	XGAppCore::UpdateWindows
 *
 *		Update the windows inside of me. This scans to see if there are
 *	any windows that need updating, and updates them all. Note that by
 *	collapsing my update events and waiting until there are no events
 *	for me to process, this emulates the behavior used on the Macintosh
 *	and Windows better.
 */

void XGAppCore::UpdateWindows()
{
	XGWindow *w;
	XRect r;
	
	for (w = XGWindow::gList; w != NULL; w = w->fNext) {
		if (w->fUpdateRgn) {
			XRectangle rr;
			
			::XClipBox(w->fUpdateRgn,&rr);
			r.left = rr.x;
			r.top = rr.y;
			r.right = r.left + rr.width;
			r.bottom = r.top + rr.height;
			
			XGDraw::UpdateRect(&r);
			XGDraw::UpdateRegion(w->fUpdateRgn);
			w->ProcessDraw(r);
			XGDraw::UpdateRegion();
			XGDraw::UpdateRect();
			
			::XDestroyRegion(w->fUpdateRgn);
			w->fUpdateRgn = 0;
		}
	}
}

/*	XGAppCore::DoEvent
 *
 *		Do one pass through my event loop.
 */

void XGAppCore::DoEvent()
{
	XGPopWindow *pptr;
	XGWindow *wptr;
	int i,len;
	short flags;
	short key;
	char buffer[16];
	KeySym sym;
	Window parent,child;
	static bool gDblClick = false;
	static clock_t gClickTime;
	static XPoint gClickWhere;
	clock_t eventTime;
	struct tms buf;
	short derFlags;
	XPoint pt;
	bool hold;
	int xtmp,ytmp;
	XEvent nevent;
	unsigned int btnmask;

	try {
		GMenuCommand = -1;
		
		/*
		 *	Poll Xlib for the next event. I don't want to hang
		 *	on XNextEvent, as I also want to process periodic
		 *	events.
		 */

		if (::XPending(_GDisplay)) {
			::XNextEvent(_GDisplay,&gLastMessage);
		} else {
			clock_t texpire;
			bool flag;
			
			/*
			 *	No events pending. I don't want to idle more than
			 *	1/4th of a second, so basically spin and call the
			 *	XEventsQueued to read the input stream from the
			 *	server. If I get something, bump out early.
			 */
			 
			UpdateWindows();				// find windows that need updating
			
			flag = false;
			texpire = times(&buf) + CLK_TCK/4;
			while (texpire > times(&buf)) {
				if (::XEventsQueued(_GDisplay,QueuedAfterReading)) {
					flag = true;
					break;
				}
				sched_yield();				// yield to other processes
			}
			if (flag) {
				/*
				 *	We received an event, so process it.
				 */
				
				::XNextEvent(_GDisplay,&gLastMessage);
			} else {
				/*
				 *	No event; just return
				 */
				
				ArrangeOrder();		// make sure windows correct
				return;
			}
		}
		eventTime = times(&buf);
		
		if (FilterEvent((void *)&gLastMessage)) {
			/* Event was processed. Bail. */
			ArrangeOrder();
			return;
		}
		if (1 == XGDispatch::SendDispatch(KXGAppEvent,0,(void *)&gLastMessage)) {
			ArrangeOrder();
			return;
		}

		/*
		 *	Record the owner of this event; this becomes my
		 *	instantaneous frontmost window
		 */
		
		_GEventWindow = gLastMessage.xany.window;
		fEventOwner = XGWindow::GetXGWindow(gLastMessage.xany.window);

		/*
		 *	We've got an Xlib style event. parse
		 */
	
		switch (gLastMessage.type) {
			case Expose:				
				/*
				 *	This is an update/draw event. Accumulate the update
				 *	area so I can process this later when there are no
				 *	events that need attention.
				 */
			
				wptr = XGWindow::GetXGWindow(gLastMessage.xexpose.window);
				if (wptr) {
					XRectangle r;
					
					r.x = gLastMessage.xexpose.x;
					r.width = gLastMessage.xexpose.width;
					r.y = gLastMessage.xexpose.y;
					r.height = gLastMessage.xexpose.height;
					
					/*
					 *	Accumulate update region
					 */
					
					if (wptr->fUpdateRgn == 0) {
						wptr->fUpdateRgn = ::XCreateRegion();
					}
					::XUnionRectWithRegion(&r,wptr->fUpdateRgn,wptr->fUpdateRgn);
				}
				break;
			
			case ClientMessage:
				/*
				 *	My main window is being shut down. Set my quit flag.
				 */
			
				if (gLastMessage.xclient.message_type == _GWMProtocols) {
					if (fEventOwner) {
						if ((fModalWindow == NULL) || (fModalWindow == fEventOwner)) {
							fEventOwner->CloseWindow();
						}
					}
				}
				break;
			
			case ConfigureNotify:
				/*
				 *	The window is being resized. If this is my main window,
				 *	pass the resize down to the window processing loop
				 */
			
				XGInline::Close();		// close any inline editors
				wptr = XGWindow::GetXGWindow(gLastMessage.xconfigure.window);
				if (wptr) {
					// Pass resize to resize my children views
					wptr->ProcessSizeWindow();
				}
				break;
			
			case KeyPress:
				/*
				 *	Key press messages all run through my standard
				 *	dispatch system for current focus
				 */
			
				len = ::XLookupString(&(gLastMessage.xkey),buffer,sizeof(buffer),&sym,NULL);
				key = GetSpecialKey(sym);
				if (key) {
					/*
					 *	Special key press
					 */
				
					flags = DeriveFlags(gLastMessage.xkey.state);
					flags |= KSpecialKey;
					if (flags & KKeyMod2) {
						XGDispatch::SendDispatch(KXGMenuKey,MAKEDWORD(flags,key),NULL);
					} else {
						XGDispatch::SendDispatch(KXGKeyEvent,MAKEDWORD(flags,key),NULL);
					}
				} else {
					/*
					 *	Normal key
					 */
				
					flags = DeriveFlags(gLastMessage.xkey.state);
					for (i = 0; i < len; i++) {
						if (flags & KKeyMod1) {
							// Dispatch menu command
							GetCurMenu()->MenuKey(buffer[i]);
						} else {
							key = (unsigned char)buffer[i];
							if (key == 0x7F) key = 0x08;	// translation
							XGDispatch::SendDispatch(KXGKeyEvent,MAKEDWORD(flags,key),NULL);
						}
					}
				}
				break;
			
			case ButtonPress:
				wptr = XGWindow::GetXGWindow(gLastMessage.xbutton.window);
				if (fModalWindow) {
					XGPopWindow *pop;
					
					pop = XGPopWindow::GetPopWindow(gLastMessage.xbutton.window);
					if ((pop != NULL) && (fModalWindow != wptr)) break;
				}
				pt.h = gLastMessage.xbutton.x;
				pt.v = gLastMessage.xbutton.y;
				
				/* Double click detection */
				gDblClick = false;
				if (gClickTime > eventTime) {
					if ((gClickWhere.h - 4 < pt.h) &&
							(gClickWhere.h + 4 > pt.h) &&
							(gClickWhere.v - 4 < pt.v) &&
							(gClickWhere.v + 4 > pt.v))
						gDblClick = true;
				}
				gClickTime = eventTime + CLK_TCK/4;
				gClickWhere = pt;

				derFlags = DeriveFlags(gLastMessage.xbutton.state);
				if (gDblClick) derFlags |= KDoubleClick;
				
				if (wptr == NULL) {
					pptr = XGPopWindow::GetPopWindow(gLastMessage.xbutton.window);
					if (pptr == NULL) break;
					GCapture = pptr->ProcessMouseDown(pt,derFlags);
				} else {
					GCapture = wptr->ProcessMouseDown(pt,derFlags);
				}
							
				if (GCapture) {
					// Grab the pointer
					i = ::XGrabPointer(_GDisplay,
										gLastMessage.xbutton.window,
										False,
								   		ButtonReleaseMask |
								   			ButtonPressMask |
								   			PointerMotionMask,
								  		GrabModeAsync,
								  		GrabModeAsync,
							 			None,
										None,
										CurrentTime);
				}	
				break;
								
			case ButtonRelease:
				/*
				 *	Button up event--pass on down only.
				 */
			
				if (!GCapture) break;
				
				wptr = XGWindow::GetXGWindow(gLastMessage.xbutton.window);
				if (wptr == NULL) {
					pptr = XGPopWindow::GetPopWindow(gLastMessage.xbutton.window);
				}
				
				pt.h = gLastMessage.xbutton.x;
				pt.v = gLastMessage.xbutton.y;
				flags = DeriveFlags(gLastMessage.xbutton.state);
				if (GCapture) flags |= KCapturedMouse;
				if (gDblClick) flags |= KDoubleClick;
				::XUngrabPointer(_GDisplay,CurrentTime);
				::XFlush(_GDisplay);
				if (wptr) {
					hold = wptr->ProcessMouseUp(pt,flags,GCapture);
				} else {
					hold = pptr->ProcessMouseUp(pt,flags);
				}
				
				if (!hold) {	
					/* Now ungrab this. */
					GCapture = NULL;
				} else {
					/* Regrab this--didn't want to let go */
					::XGrabPointer(_GDisplay,
									gLastMessage.xbutton.window,
									False,
							   		ButtonReleaseMask |
							   			ButtonPressMask |
							   			PointerMotionMask,
							  		GrabModeAsync,
							  		GrabModeAsync,
						 			None,
									None,
									CurrentTime);
				}
				break;
				
			case FocusIn:
				wptr = XGWindow::GetXGWindow(gLastMessage.xfocus.window);
				if (wptr) {
					wptr->fActive = true;
					wptr->ProcessActivate(true);
				}
				break;
			case FocusOut:
				wptr = XGWindow::GetXGWindow(gLastMessage.xfocus.window);
				if (wptr) {
					wptr->fActive = false;
					wptr->ProcessActivate(false);
				}
				break;
		
			case EnterNotify:
				/*
				 *	Enter notify generates motion flag. The
				 *	view enter notification occurs as a byproduct
				 *	of calling the ProcessMouseMove routine.
				 */
			
				pt.h = gLastMessage.xcrossing.x;
				pt.v = gLastMessage.xcrossing.y;
				flags = DeriveFlags(gLastMessage.xcrossing.state);
				if (GCapture) flags |= KCapturedMouse;
				if (gDblClick) flags |= KDoubleClick;
				wptr = XGWindow::GetXGWindow(gLastMessage.xcrossing.subwindow);
				if (wptr) {
					wptr->ProcessMouseMove(pt,flags,GCapture);
				} else {
					pptr = XGPopWindow::GetPopWindow(gLastMessage.xcrossing.subwindow);
					if (pptr) pptr->ProcessMouseMove(pt,flags);
				}
				break;
			
			case LeaveNotify:
				/*
				 *	Leave notify generates view leave if a view had the
				 *	cursor...
				 */
			
				XGWindow::ProcessMouseLeave();
				break;

			case MotionNotify:
				/*
				 *	Motion notify generates mouse moved events
				 */
				
				nevent = gLastMessage;
				while (XCheckMaskEvent(_GDisplay,ButtonMotionMask,&nevent)) ;
				if (!XQueryPointer(_GDisplay,
								   nevent.xmotion.window,
								   &parent,
								   &child,
								   &xtmp,
								   &ytmp,
								   &nevent.xmotion.x,
								   &nevent.xmotion.y,
								   &btnmask)) break;
				gLastMessage = nevent;
				 
				/*
				 *	Find the window where this happened
				 */

				wptr = XGWindow::GetXGWindow(gLastMessage.xmotion.window);
				if (fModalWindow && (fModalWindow != wptr)) break;

				pt.h = gLastMessage.xmotion.x;
				pt.v = gLastMessage.xmotion.y;
				flags = DeriveFlags(gLastMessage.xcrossing.state);
				if (GCapture) flags |= KCapturedMouse;
				if (gDblClick) flags |= KDoubleClick;

				if (wptr) {
					wptr->ProcessMouseMove(pt,flags,GCapture);
				} else {
					pptr = XGPopWindow::GetPopWindow(gLastMessage.xmotion.window);
					if (pptr) {
						pptr->ProcessMouseMove(pt,flags);
					}
				}
				break;
				
			case ReparentNotify:
				/*
				 *	Sent when the window I just made is wrappered
				 *	into a parent window. This sets the parent
				 *	window attribute.
				 *
				 *	I scan until I find the window that is the child
				 *	of the root window of my screen which owns my own
				 *	window. This is the thing I have to manipulate to
				 *	arrange the order of my windows...
				 */
				
				wptr = XGWindow::GetXGWindow(gLastMessage.xreparent.window);
				if (wptr) {
					for (;;) {
						parent = GetParentWindow(gLastMessage.xreparent.parent);
						if (parent == None) break;
						gLastMessage.xreparent.parent = parent;
					}
					wptr->fWMWindow = gLastMessage.xreparent.parent;
				}
				break;
			default:
				break;
		}
		
		/*
		 *	If I get here, there is a menu command to perform
		 */
		
		if (GMenuCommand > 0) {
			if (wptr) {
				if (wptr->fFocus) wptr->fFocus->SendDispatch(KXGDoMenuCommand,GMenuCommand,NULL);
				else wptr->SendDispatch(KXGDoMenuCommand,GMenuCommand,NULL);
			}
			GMenuCommand = -1;
		}
	}
	catch (XGError &x) {
		x.DisplayError();
	}
	/* Handle gcc bug with exception handling */
	catch (XGPostError &x) {
		x.DisplayError();
	}
	catch (XGAbort &x) {
		x.DisplayError();
	}
	
	ArrangeOrder();					/* Make sure order of windows is correct */
	fEventOwner = NULL;
}

/*	XGAppCore::CancelCheck
 *
 *		Check to see if the cancel button is pressed. This doesn't
 *	do a thing right now...
 */

bool XGAppCore::CancelCheck()
{
	return false;
}

#endif // OPT_XWIN
