/*	XGThread.cpp
 *
 *		Thread support. This allows me to encapsulate simple thread
 *	semantics behind a simple object declaration. And this allows me
 *	to use the same semantics on both Windows, the MacOS and MkLinux
 *	X Windows.
 *
 *		If OPT_THREADED == 0, this will be built without thread
 *	support. This should work, so long as you don't call new XGThread.
 *	Of course things are pretty boring...
 */

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

  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>			// must be first to optimize compile on MSVC++
#include <XError.h>
#include <XThread.h>

#if OPT_WINOS
	#ifdef __MWERKS__
	#define _MT
	#endif
#include <process.h>			// _beginthreadex/_endthreadex
#endif

/************************************************************************/
/*																		*/
/*	Globals																*/
/*																		*/
/************************************************************************/

long XGThread::gCount = 0;

#if OPT_THREADED == 1

static XGSemaphore GMutex;
XGThread *XGThread::gList = NULL;

#if OPT_WINOS == 1
extern DWORD GAppThreadID = -1;
#endif // OPT_WINOS

#if OPT_MACOS
long XGThread::gStackSize = 0;
bool XGThread::gThreadPool = false;
bool GYieldThread;					/* Yield rest of slice? */

bool XGThread::gInitTM = false;
bool XGThread::gHasTM = false;
#endif

#if OPT_XWIN
static pthread_t GAppThreadID;
#endif

#endif // OPT_THREADED

static XGThread GAppThread;			/* Application thread object		*/

/************************************************************************/
/*																		*/
/*	Thread Support														*/
/*																		*/
/************************************************************************/

/*$	XGThread::XGThread
 *
 *		Create this thread. Calls the procudure specified inside the
 *	thread. The 'arg' field gives the four-byte argument which is used
 *	to invoke the provided procedure. The 'stack' object gives the
 *	size of the stack; if zero, the stack is set to the default size.
 *	And 'suspend', if true, creates this thread in the suspended state.
 */

XGThread::XGThread(XGThreadProcPtr proc, long arg, long stack, bool suspend)
{
	if (!HasThreadManager()) throw XGPostError(KXGThreadNotSupported);

	gCount++;
	
#if OPT_THREADED == 1
	/*
	 *	Threaded support
	 */
	
	fArg = arg;
	
#if OPT_MACOS == 1
	/*
	 *	Mac version
	 */

	OSErr err;
	
	/*
	 *	Create this thread. The thread is built suspended so I can
	 *	have a chance to do some initialization before it runs.
	 */
	
	if (stack == 0) stack = gStackSize;
	fThreadID = kNoThreadID;
	fProc = proc;
	fAttach = 1;					// One owner--me
	if (0 != (err = NewThread(kCooperativeThread,	// only type on MacOS
							  ThreadEntry,			// my entry point
							  (void *)this,			// and my argument to entry
							  stack,				// stack size default
							  (!gThreadPool ?		// suspended at first
							  	kNewSuspend :
							  	(kNewSuspend | kUsePremadeThread)),
							  NULL,					// Thread result--who cares?
							  &fThreadID)))			// Thread ID of me
		throw XGPostError(KXGThreadCreateError);
#endif

#if OPT_WINOS == 1
	/*
	 *	Windows 95/Windows NT version: what's the default thread ID?
	 */
	
	if (GAppThreadID == -1) {
		GAppThreadID = GetCurrentThreadId();
	}
	
	/*
	 *	Create this thread. The thread is built suspended so I can
	 *	have a chance to do some initialization before it runs
	 */
	
	fProc = proc;
	fAttach = 1;
	fThreadHandle = (HANDLE)_beginthreadex(NULL,	// No security attributes
								   stack,			// default stack size
								   ThreadEntry,		// my entry point
								   (LPVOID)this,	// and my args
								   CREATE_SUSPENDED,// create me suspended
								   &fThreadID);		// and what am I?
	if (fThreadHandle == NULL) {
		throw XGPostError(KXGThreadCreateError);
	}
	
	fThreadSuspend = true;
#endif

#if OPT_XWIN == 1
	/*
	 *	<Pthreads> version
	 */
	
	fThreadSuspend = suspend;
	if (pthread_create(&fThreadID,NULL,ThreadEntry,(void *)this)) {
		throw XGPostError(KXGThreadCreateError);
	}
#endif
	
	/*
	 *	Insert thread into linked list
	 */
	
	GMutex.EnterCritical();
	fNext = gList;
	gList = this;
	GMutex.LeaveCritical();

	/*
	 *	And resume the thread if it's what the user wants
	 */
	
	if (!suspend) Resume();

#else // OPT_THREADED == 0
	#pragma unused(proc)
	#pragma unused(arg)
	#pragma unused(stack)
	#pragma unused(suspend)
	
	throw XGPostError(KXGThreadNotSupported);
#endif
}

/*$	XGThread::XGThread
 *
 *		This is a special constructor, which should never be called by
 *	another procedure, which creates the thread which represents the main
 *	task.
 */

XGThread::XGThread()
{
	gCount++;

#if OPT_THREADED == 1

	/*
	 *	Threaded support
	 */
	
	GMutex.EnterCritical();
	
	fProc = NULL;
	fNext = gList;
	gList = this;

#if OPT_MACOS == 1
	fThreadID = kApplicationThreadID;
#endif

#if OPT_WINOS == 1
	if (GAppThreadID == -1) GAppThreadID = GetCurrentThreadId();
	fThreadID = GAppThreadID;
	fThreadHandle = NULL;
#endif

#if OPT_XWIN == 1
	fThreadID = pthread_self();
	GAppThreadID = fThreadID;
#endif
	
	GMutex.LeaveCritical();

#endif // OPT_THREADED
}

/*$	XGThread::~XGThread
 *
 *		Stop this thing if it's running, and release the memory associated
 *	with this object. To be safe, you should probably call Detach() instead.
 *	Killing threads are not necessarly well supported across different
 *	platforms. In particular, the Unix pthreads library doesn't support
 *	abnormal thread termination.
 *
 *		If you do need to kill a thread, it's best to set up some form
 *	of semaphore with a 'terminate' signal. That way, you can also have
 *	your thread perform all necessary thread termination itself.
 */

XGThread::~XGThread()
{
#if OPT_THREADED == 1

#if OPT_MACOS == 1
	ThreadID t;
	
	if ((fThreadID != kApplicationThreadID) && (fThreadID != kNoThreadID)) {
		/*
		 *	We've got to kill this first
		 */
		
		t = fThreadID;
		fThreadID = kNoThreadID;
		DisposeThread(t,NULL,(gThreadPool == NULL));
	}
#endif

#if OPT_WINOS == 1
	if (fThreadHandle) Kill();
#endif

#if OPT_XWIN == 1
	if (!pthread_equal(fThreadID,GAppThreadID)) Kill();
#endif
	
	/*
	 *	Remove thread from linked list
	 */
	
	XGThread *thread;

	GMutex.EnterCritical();
	if (gList == this) gList = fNext;
	else {
		for (thread = gList; thread != NULL; thread = thread->fNext) {
			if (thread->fNext == this) {
				thread->fNext = fNext;
				break;
			}
		}
	}
	GMutex.LeaveCritical();
	
#endif // OPT_THREADED

	gCount--;
}

/************************************************************************/
/*																		*/
/*	Thread Manager present?												*/
/*																		*/
/************************************************************************/

/*$	XGThread::HasThreadManager
 *
 *		Not all platforms supported by YAAF has a thread manager
 *	present. Some platforms do not have thread support, and so
 *	this provides a mechanism for determining if threading is
 *	present
 */

bool XGThread::HasThreadManager()
{
#if OPT_THREADED
#if OPT_MACOS
	OSErr err;
	long flags;
	
	/*
	 *	Macintosh note:
	 *
	 *		This gets called before my MacOS initialization code
	 *	in XGAppCore::InitOS is called.
	 *
	 *		It turns out that on versions of the MacOS earlier than
	 *	OS v6.0.5, Gestalt is not present. This call (_GestaltPatch)
	 *	is defined in XGAppCore_mac.cpp, and provides a hook for
	 *	calling either Gestalt, or SysEnvirons if Gestalt is not
	 *	present.
	 */
	
	extern pascal OSErr _GestaltPatch(OSType, long *);
	
	/*
	 *	Earlier versions of the MacOS may not have the thread manager.
	 *	This determines if the thread manager is present.
	 */
	
	if (gInitTM) return gHasTM;
	err = _GestaltPatch(gestaltThreadMgrAttr,&flags);
	if (err) {
		gHasTM = false;
	} else {
		gHasTM = (flags & 1) ? true : false;
	}
	gInitTM = true;
	return gHasTM;
#else
	return true;
#endif
#else
	return false;
#endif
}

/************************************************************************/
/*																		*/
/*	Thread Management													*/
/*																		*/
/************************************************************************/

/*$	XGThread::YieldThread
 *
 *		Yields time to another thread. If this is running on a
 *	platform that does not support threading, this does nothing.
 *	Only a thread may call it's own "YieldThread()" routine; for
 *	another thread to call this is an assertion failure.
 *
 *		The value provided is set to 'true' if this should yield
 *	the rest of my processor timeslice to another thread. Normally
 *	you would call this with 'true' if you are waiting for another
 *	operation, such as I/O or a semaphore.
 *
 *		Even on the Macintosh, this flag is used to determine if
 *	the main event loop should call WaitNextEvent() with a nice,
 *	large value, or GetNextEvent() and hog all the processor cycles.
 *	That way, you can continue to be responsive to other applications,
 *	but continue to get as many processor cycles as possible during
 *	a computationally intensive loop.
 *
 *		Unix doesn't provide a mechanism for suspending thread
 *	execution. However, if the thread has been asked to be suspended,
 *	this will spin lock until the suspend flag is cleared. That
 *	permits me to emulate a third-party suspending an execution
 *	thread. Note that if you are using thread suspention by a third
 *	party thread to permit the thread's state to be tampered with,
 *	you should use semaphores and global memory instead.
 */

void XGThread::YieldThread(bool f)
{
	if (!HasThreadManager()) return;

#if OPT_THREADED == 1	
	/*
	 *	Threads are supported
	 */
	
#if OPT_MACOS == 1
	if (!f) GYieldThread = false;	// true if in processor-intensive loop
	YieldToAnyThread();				/* Always yield computational time */
#endif

#if OPT_WINOS == 1
	if (f) ::Sleep(0);				/* Yield rest of my timeslice */
#endif

#if OPT_XWIN == 1
	XGThread *thread;

	if (f) {
		sched_yield();			// yield rest of my timeslice
	}
	
	thread = CurrentThread();
	if (thread) while (thread->fThreadSuspend) {
		/*
		 *	Have I been asked to suspend myself? If so, continue to
		 *	spin here until the flag is cleared by "Resume()".
		 */
		
		sched_yield();
	}
#endif

#else // OPT_THREADED == 0
	#pragma unused(f)
#endif // OPT_THREADED
}

/*$	XGThread::Sleep
 *
 *		Put thread to sleep for a while (duration in 100ths of a second).
 *	If this is not operating on a system which supports thread sleep (such
 *	as on Unix???), or on a system which does not support threading, this
 *	does nothing.
 */

void XGThread::Sleep (long duration)
{
	if (!HasThreadManager()) throw XGPostError(KXGThreadNotSupported);

#if OPT_THREADED == 1
#if OPT_MACOS == 1
	XGThread *thr = CurrentThread();
	unsigned long deadline = TickCount() + (duration * 3) / 5;	/* duration * 60/100 */
	do {
		YieldThread(true);
	} while (thr != NULL && thr->fThreadID != kNoThreadID && deadline > TickCount());
#endif

#if OPT_WINOS == 1
	::Sleep(duration * 10);
#endif

#if OPT_XWIN == 1
	// sleep(duration / 100)?
#endif
#else // OPT_THREADED == 0
	/*
	 *	No thread support
	 */
#endif
}


#if OPT_MACOS

/*$	XGThread::SetThreadPool
 *
 *		Macintosh specific; calls the 'CreateThreadPool' routine in
 *	the thread manager. This is a useful call on the Macintosh in
 *	that it allows you to allocate an array of thread pools if you
 *	know you are going to be creating a bunch of threads.
 */

void XGThread::SetThreadPool(short pool, long stack)
{
	if (!HasThreadManager()) return;

	gStackSize = stack;
	if (CreateThreadPool(kCooperativeThread,pool,stack)) {
		throw XGPostError(KXGOutMemory);
	}
	gThreadPool = true;
}

#endif

/*$	XGThread::CurrentThread
 *
 *		This returns a pointer to the current thread object. This
 *	even returns a "dummy" thread object for the main thread.
 */

XGThread *XGThread::CurrentThread()
{
	if (!HasThreadManager()) return &GAppThread;
	
#if OPT_THREADED == 1
	/*
	 *	Threads are supported
	 */
	
	XGThread *t;

#if OPT_MACOS == 1
	ThreadID id;
	
	// Find the thread identifier on the Macintosh
	if (GetCurrentThread(&id)) return &GAppThread;
	if (id == kApplicationThreadID) return &GAppThread;
#endif

#if OPT_WINOS == 1
	DWORD id;
	
	// Find the thread identifier on Windows
	id = GetCurrentThreadId();
	if (id == GAppThreadID) return &GAppThread;
#endif

#if OPT_XWIN == 1
	pthread_t curthread;
	curthread = pthread_self();
	if (pthread_equal(curthread,GAppThreadID)) return &GAppThread;
#endif

	// Search my linked list until I find the XGThread object which
	// corrisponds to this OS thread
	GMutex.EnterCritical();
	for (t = gList; t != NULL; t = t->fNext) {
	
#if OPT_XWIN
		if (pthread_equal(curthread,t->fThreadID)) break;
#else
		if (t->fThreadID == id) break;
#endif
	}
	GMutex.LeaveCritical();
	
	// If not found, punt with a global application thread identifier
	if (t == NULL) return &GAppThread;
	return t;
	
#else // OPT_THREADED == 1
	/*
	 *	No thread support--punt
	 */
	
	return &GAppThread;				// Always return same identifier
#endif // OPT_THREADED
}

/************************************************************************/
/*																		*/
/*	Thread Management													*/
/*																		*/
/************************************************************************/

/*$	XGThread::Suspend
 *
 *		Suspend the specified thread. On Unix or on a platform which
 *	does not support threading, this does nothing. Otherwise, execution
 *	is halted until someone else calls the 'Resume' method.
 *
 *		On Unix, thread suspention by a third party thread is "emulated"
 *	by trapping for the suspend flag in the 'YieldThread()' method. If
 *	you are calling this on yourself, however, this will guarentee
 *	suspention right away by calling the YieldThread() (and going into
 *	a spin lock) until another thread calls the 'resume' method.
 */

void XGThread::Suspend(void)
{
	if (!HasThreadManager()) throw XGPostError(KXGThreadNotSupported);

#if OPT_THREADED == 1
#if OPT_MACOS == 1
	if (fThreadID != kNoThreadID) {
		::SetThreadState(fThreadID,kStoppedThreadState,kNoThreadID);
	}
#endif

#if OPT_WINOS == 1
	if (!IsSuspend() && (fThreadHandle != NULL)) {
		::SuspendThread(fThreadHandle);
		fThreadSuspend = false;
	}
#endif

#if OPT_XWIN == 1
	if (this != &GAppThread) {
		fThreadSuspend = true;
		if (this == CurrentThread()) YieldThread(false);
	}
#endif

#endif // OPT_THREADED
}

/*$	XGThread::Resume
 *
 *		Resume execution with the specified thread. If this thread
 *	is currently running, this does nothing. Otherwise, the thread
 *	will get time at the next available timeslice.
 */

void XGThread::Resume(void)
{
	if (!HasThreadManager()) throw XGPostError(KXGThreadNotSupported);

#if OPT_THREADED == 1
#if OPT_MACOS == 1
	if (fThreadID != kNoThreadID) {
		SetThreadState(fThreadID,kReadyThreadState,kNoThreadID);
	}
#endif

#if OPT_WINOS == 1
	if (IsSuspend() && (fThreadHandle != NULL)) {
		::ResumeThread(fThreadHandle);
		fThreadSuspend = true;
	}
#endif

#if OPT_XWIN == 1
	fThreadSuspend = false;
#endif

#endif // OPT_THREADED
}

/*$	XGThread::IsSuspend
 *
 *		Is this thread suspended? This returns 'true' if the thread's
 *	execution is currently suspended.
 */

bool XGThread::IsSuspend(void)
{
	if (!HasThreadManager()) return false;

#if OPT_THREADED == 1
#if OPT_MACOS == 1
	ThreadState state;
	
	if (fThreadID == kNoThreadID) return true;		/* Like it's not running */
	GetThreadState(fThreadID,&state);
	return (state == kStoppedThreadState) ? true : false;
#endif

#if OPT_WINOS == 1
	return fThreadSuspend;
#endif

#if OPT_XWIN == 1
	return fThreadSuspend;
#endif
#else // OPT_THREADED == 0
	/*
	 *	No thread support--simply return false: I never suspend
	 */
	
	return false;
#endif
}

/*$	XGThread::Kill
 *
 *		Kill this thread. This cancels execution of this thread.
 *	This is a rude thing to do, as the thread will not have a chance
 *	to clean up after itself.
 */

void XGThread::Kill(void)
{
	if (!HasThreadManager()) throw XGPostError(KXGThreadNotSupported);

#if OPT_THREADED == 1
	/*
	 *	Threads supported
	 */
	
#if OPT_MACOS == 1
	ThreadID t;
	
	if (fThreadID == kNoThreadID) return;
	Assert(fThreadID != kApplicationThreadID);
	
	t = fThreadID;
	fThreadID = kNoThreadID;
	DisposeThread(t,NULL,(gThreadPool == NULL));
#endif

#if OPT_WINOS == 1
	if (fThreadHandle) {
		TerminateThread(fThreadHandle,0);
		CloseHandle(fThreadHandle);
		fThreadHandle = NULL;
		Detach();
	}
#endif

#if OPT_XWIN == 1
	pthread_cancel(fThreadID);
	fThreadValid = false;
#endif
#else // OPT_THREADED == 0
	Assert(false);
#endif
}

/*$	XGThread::IsAlive
 *
 *		Is this thread alive? This returns 'true' if the thread is
 *	currently "alive", even though it may not be currently exeucting.
 *	This returns 'false' if the thread has already terminated and this
 *	is a zombie. (Note that unless you have used the Attach() method
 *	to maintain a pointer to this thread, the thread object will be
 *	automatically released on termination of the thread procedure.)
 */

bool XGThread::IsAlive(void)
{
	if (!HasThreadManager()) return true;

#if OPT_THREADED == 1
	/*
	 *	Threads supported
	 */
	
#if OPT_MACOS == 1
	return (fThreadID != NULL) ? true : false;
#endif

#if OPT_WINOS == 1
	if (fThreadID == GAppThreadID) return true;
	if (fThreadHandle != NULL) return true;
	return false;
#endif

#if OPT_XWIN == 1
	return fThreadValid;
#endif

#else // OPT_THREADED == 0
	/*
	 *	No threads. Which means that this is my global app. Which
	 *	means I'm alive
	 */
	
	return true;
#endif
}

/************************************************************************/
/*																		*/
/*	Internal thread utilities											*/
/*																		*/
/************************************************************************/

#if OPT_THREADED == 1

#if OPT_MACOS == 1

/*	XGThread::ThreadEntry
 *
 *		This is the actual thread entry point. This wrappers some stuff
 *	for my thread. This contains the throw/catch routines to prevent
 *	tossing this exception across the stack boundary.
 */

pascal void *XGThread::ThreadEntry(void *ptr)
{
	XGThread *tptr = (XGThread *)ptr;
	
	try {
		if (tptr->fProc) (tptr->fProc)(tptr,tptr->fArg);
	}
	catch (XGError &x) {
		// If it's one of mine, do the default error display
		x.DisplayError();
	}

	catch (...) {
		// all the rest get eaten and absorbed.
		XGPostError te(KXGRandomException);
		te.DisplayError();
	}
	
	if (gThreadPool) DisposeThread(tptr->fThreadID,NULL,true);
	tptr->fThreadID = kNoThreadID;
	tptr->Detach();
	return NULL;
}

#endif // OPT_MACOS

#if OPT_WINOS == 1

/*	XGThread::ThreadEntry
 *
 *		The entrypoint for the Windows application
 */

unsigned int WINAPI XGThread::ThreadEntry(LPVOID ptr)
{
	XGThread *tptr = (XGThread *)ptr;
	
	try {
		if (tptr->fProc) (tptr->fProc)(tptr,tptr->fArg);
	}
	catch (XGError &x) {
		// If it's one of mine, do the default error display
		x.DisplayError();
	}
	catch (...) {
		// all the rest get eaten and absorbed.
		XGPostError te(KXGRandomException);
		te.DisplayError();
	}

	tptr->fThreadHandle = NULL;
	tptr->Detach();
	return 0;
}

#endif // OPT_WINOS

#if OPT_XWIN == 1

/*	XGThread::ThreadEntry
 *
 *		The entrypoint for Posix-based threaded application
 */

void *XGThread::ThreadEntry(void *ptr)
{
	XGThread *tptr = (XGThread *)ptr;
	
	tptr->fThreadValid = true;
	// Was I created suspended? If so, spin lock.
	if (tptr->fThreadSuspend) YieldThread(false);
	try {
		if (tptr->fProc) (tptr->fProc)(tptr,tptr->fArg);
	}
	catch (XGError &x) {
		// If it's one of mine, do the default error display
		x.DisplayError();
	}
	catch (...) {
		// all the rest get eaten and absorbed.
		XGPostError te(KXGRandomException);
		te.DisplayError();
	}

	tptr->fThreadValid = false;
	tptr->Detach();
	return 0;
}

#endif // OPT_WINOS

#endif // OPT_THREADED


