/******************************************************************************
 JXSelectionManager.cc

	Global object to interface with X Selection mechanism.

	Nobody else seems to support MULTIPLE, so I see no reason to support
	it here.  If we ever need to support it, here is the basic idea:

		Pass in filled JArray<Atom>* and empty JArray<char*>*
		We remove the unconverted targets from JArray<Atom> and
			fill in the JArray<char*> with the converted data
		We can optimize this to directly call the widget that owns
			the selection.

	BASE CLASS = virtual JBroadcaster

	Copyright © 1996-98 by John Lindal. All rights reserved.

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

#include <JXSelectionManager.h>
#include <JXDNDManager.h>
#include <JXDisplay.h>
#include <JXWindow.h>
#include <JXWidget.h>
#include <jXGlobals.h>
#include <jTime.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <X11/Xlib.h>
#include <iostream.h>
#include <jAssert.h>

#define JXSEL_DEBUG_MSGS		0	// boolean
#define JXSEL_DEBUG_ONLY_RESULT 0	// boolean
#define JXSEL_MICRO_TRANSFER	0	// boolean

const clock_t kWaitForSelectionTime = 5 * CLOCKS_PER_SEC;
const clock_t kUserBoredWaitingTime = 1 * CLOCKS_PER_SEC;

static const JCharacter* kSWPXAtomName             = "JXSelectionWindowProperty";
static const JCharacter* kIncrementalXAtomName     = "INCR";
static const JCharacter* kTargetsXAtomName         = "TARGETS";
static const JCharacter* kTimeStampXAtomName       = "TIMESTAMP";
static const JCharacter* kTextXAtomName            = "TEXT";
static const JCharacter* kCompoundTextXAtomName    = "COMPOUND_TEXT";
static const JCharacter* kMultipleXAtomName        = "MULTIPLE";
static const JCharacter* kMimePlainTextXAtomName   = "text/plain";
static const JCharacter* kURLXAtomName             = "text/uri-list";
static const JCharacter* kDeleteSelectionXAtomName = "DELETE";
static const JCharacter* kNULLXAtomName            = "NULL";

/******************************************************************************
 Constructor

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

JXSelectionManager::JXSelectionManager
	(
	JXDisplay* display
	)
{
	itsDisplay = display;

	itsOwnerList = new JArray<OwnerInfo>;
	assert( itsOwnerList != NULL );
	itsOwnerList->SetCompareFunction(CompareSelectionNames);

	itsMaxDataChunkSize = XMaxRequestSize(*display) * 4/5;

	itsReceivedAllocErrorFlag  = kFalse;
	itsTargetWindow            = None;
	itsTargetWindowDeletedFlag = kFalse;

	// create required X atoms

	itsSelectionWindPropXAtom = itsDisplay->RegisterXAtom(kSWPXAtomName);
	itsIncrementalSendXAtom   = itsDisplay->RegisterXAtom(kIncrementalXAtomName);

	itsTargetsXAtom           = itsDisplay->RegisterXAtom(kTargetsXAtomName);
	itsTimeStampXAtom         = itsDisplay->RegisterXAtom(kTimeStampXAtomName);
	itsTextXAtom              = itsDisplay->RegisterXAtom(kTextXAtomName);
	itsCompoundTextXAtom      = itsDisplay->RegisterXAtom(kCompoundTextXAtomName);
	itsMultipleXAtom          = itsDisplay->RegisterXAtom(kMultipleXAtomName);
	itsMimePlainTextXAtom     = itsDisplay->RegisterXAtom(kMimePlainTextXAtomName);
	itsURLXAtom               = itsDisplay->RegisterXAtom(kURLXAtomName);

	itsDeleteSelectionXAtom   = itsDisplay->RegisterXAtom(kDeleteSelectionXAtomName);
	itsNULLXAtom              = itsDisplay->RegisterXAtom(kNULLXAtomName);
}

/******************************************************************************
 Destructor

	We should not be deleted until all JXWidgets have been deleted.

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

JXSelectionManager::~JXSelectionManager()
{
#ifndef NDEBUG
	{
	const JSize count = itsOwnerList->GetElementCount();
	for (JIndex i=1; i<=count; i++)
		{
		const OwnerInfo info = itsOwnerList->GetElement(i);
		assert( info.owner == NULL );
		}
	}
#endif

	delete itsOwnerList;
}

/******************************************************************************
 GetAvailableTypes

	window should be the window that X should attach the data to.
	Widgets can simply pass in the result from GetWindow().

	time can be CurrentTime.

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

JBoolean
JXSelectionManager::GetAvailableTypes
	(
	const Atom		selectionName,
	const Time		time,
	const JXWindow*	window,
	JArray<Atom>*	typeList
	)
{
	// Check if this application owns the selection.

	JXWidget* owner = NULL;
	const JArray<Atom>* localTypeList = NULL;
	if (GetSelectionOwner(selectionName, time, &owner) &&
		owner->GetSelectionTargets(selectionName, &localTypeList))
		{
		*typeList = *localTypeList;
		return kTrue;
		}

	// We have to go via the X server.

	const Window xWindow = window->GetXWindow();

	XSelectionEvent selEvent;
	if (RequestSelectionData(selectionName, time, xWindow, itsTargetsXAtom, &selEvent))
		{
		Atom actualType;
		int actualFormat;
		unsigned long itemCount, remainingBytes;
		unsigned char* data = NULL;
		XGetWindowProperty(*itsDisplay, xWindow, itsSelectionWindPropXAtom,
						   0, LONG_MAX, True, XA_ATOM,
						   &actualType, &actualFormat,
						   &itemCount, &remainingBytes, &data);

		typeList->RemoveAll();
		if (actualType == XA_ATOM &&
			actualFormat/8 == sizeof(Atom) && remainingBytes == 0)
			{
			itemCount /= actualFormat/8;

			Atom* atomData = reinterpret_cast<Atom*>(data);
			for (JIndex i=1; i<=itemCount; i++)
				{
				typeList->AppendElement(atomData[i-1]);
				}

			XFree(data);
			return kTrue;
			}
		else
			{
			XFree(data);
			return kFalse;
			}
		}
	else
		{
		return kFalse;
		}
}

/******************************************************************************
 GetSelectionData

	window should be the one that X should attach the data to.
	Widgets can simply pass in the result from GetWindow().

	time can be CurrentTime.

	*** Caller is responsible for calling DeleteSelectionData() on the data
		that is returned.

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

JBoolean
JXSelectionManager::GetSelectionData
	(
	const Atom		selectionName,
	const Time		time,
	const JXWindow*	window,
	const Atom		requestType,
	Atom*			returnType,
	unsigned char**	data,
	JSize*			dataLength,
	DeleteMethod*	delMethod
	)
{
	// Check if this application owns the selection.

	JXWidget* owner = NULL;
	JSize bitsPerBlock;
	if (GetSelectionOwner(selectionName, time, &owner))
		{
		if (owner->MainConvertSelection(selectionName, requestType, returnType,
										data, dataLength, &bitsPerBlock))
			{
			*delMethod = kArrayDelete;
			return kTrue;
			}
		else
			{
			*returnType = None;
			*data       = NULL;
			*dataLength = 0;
			return kFalse;
			}
		}

	// We have to go via the X server.

	*returnType = None;
	*data       = NULL;
	*dataLength = 0;
	*delMethod  = kXFree;

	const Window xWindow = window->GetXWindow();

	XSelectionEvent selEvent;
	if (RequestSelectionData(selectionName, time, xWindow, requestType, &selEvent))
		{
		#if JXSEL_DEBUG_MSGS && ! JXSEL_DEBUG_ONLY_RESULT
		cout << "Received SelectionNotify" << endl;
		#endif

		// We need to discard all existing PropertyNotify events
		// before initiating the incremental transfer.
		{
		XEvent xEvent;
		XID checkIfEventData[] = { xWindow, itsSelectionWindPropXAtom };
		while (XCheckIfEvent(*itsDisplay, &xEvent, GetNextNewPropertyEvent,
							 reinterpret_cast<char*>(checkIfEventData)))
			{
			// ignore the event
			}
		}

		// Initiate incremental transfer by deleting the property.

		int actualFormat;
		unsigned long itemCount, remainingBytes;
		XGetWindowProperty(*itsDisplay, xWindow, itsSelectionWindPropXAtom,
						   0, LONG_MAX, True, AnyPropertyType,
						   returnType, &actualFormat,
						   &itemCount, &remainingBytes, data);

		if (*returnType == itsIncrementalSendXAtom)
			{
			XFree(*data);
			return ReceiveSelectionDataIncr(selectionName, xWindow, returnType,
											data, dataLength, delMethod);
			}
		else if (*returnType != None && remainingBytes == 0)
			{
			*dataLength = itemCount * actualFormat/8;
			return kTrue;
			}
		else
			{
			XFree(*data);
			*data = NULL;
			return kFalse;
			}
		}
	else
		{
		return kFalse;
		}
}

/******************************************************************************
 DeleteSelectionData

	This must be called with all data returned by GetSelectionData().
	The DeleteMethod must be the method returned by GetSelectionData().

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

void
JXSelectionManager::DeleteSelectionData
	(
	unsigned char**		data,
	const DeleteMethod	delMethod
	)
{
	if (delMethod == kXFree)
		{
		XFree(*data);
		}
	else if (delMethod == kCFree)
		{
		free(*data);
		}
	else
		{
		assert( delMethod == kArrayDelete );
		delete [] *data;
		}

	*data = NULL;
}

/******************************************************************************
 SendDeleteSelectionRequest

	Implements the DELETE selection protocol.

	window should be any one that X can attach the data to.
	Widgets can simply pass in the result from GetWindow().

	time can be CurrentTime.

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

void
JXSelectionManager::SendDeleteSelectionRequest
	(
	const Atom		selectionName,
	const Time		time,
	const JXWindow*	window
	)
{
	Atom returnType;
	unsigned char* data = NULL;
	JSize dataLength;
	JXSelectionManager::DeleteMethod delMethod;

	GetSelectionData(selectionName, time, window, itsDeleteSelectionXAtom,
					 &returnType, &data, &dataLength, &delMethod);
	DeleteSelectionData(&data, delMethod);
}

/******************************************************************************
 RequestSelectionData (private)

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

JBoolean
JXSelectionManager::RequestSelectionData
	(
	const Atom			selectionName,
	const Time			origTime,
	const Window		xWindow,
	const Atom			type,
	XSelectionEvent*	selEvent
	)
{
	assert( type != None );

	JXDNDManager* dndMgr = itsDisplay->GetDNDManager();

	Time time = origTime;
	if (time == CurrentTime)
		{
		time = itsDisplay->GetLastEventTime();
		}

	XConvertSelection(*itsDisplay, selectionName, type,
					  itsSelectionWindPropXAtom, xWindow, time);

	Bool receivedEvent = False;
	XEvent xEvent;
	const clock_t startTime = clock();
	const clock_t endTime   = startTime + kWaitForSelectionTime;
	JBoolean userBored      = kFalse;
	while (!receivedEvent && clock() < endTime)
		{
		receivedEvent =
			XCheckTypedWindowEvent(*itsDisplay, xWindow, SelectionNotify, &xEvent);

		if (!userBored && clock() > startTime + kUserBoredWaitingTime)
			{
			userBored = kTrue;
			(JXGetApplication())->DisplayBusyCursor();
			}
		}

	if (receivedEvent)
		{
		assert( xEvent.type == SelectionNotify );
		*selEvent = xEvent.xselection;
		return JI2B(selEvent->requestor == xWindow &&
					selEvent->selection == selectionName &&
					selEvent->target    == type &&
					selEvent->property  == itsSelectionWindPropXAtom );
		}
	else
		{
		return kFalse;
		}
}

/******************************************************************************
 HandleSelectionRequest

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

void
JXSelectionManager::HandleSelectionRequest
	(
	const XSelectionRequestEvent& selReqEvent
	)
{
	XEvent xEvent;
	XSelectionEvent& returnEvent = xEvent.xselection;

	returnEvent.type      = SelectionNotify;
	returnEvent.display   = selReqEvent.display;
	returnEvent.requestor = selReqEvent.requestor;
	returnEvent.selection = selReqEvent.selection;
	returnEvent.target    = selReqEvent.target;
	returnEvent.property  = selReqEvent.property;
	returnEvent.time      = selReqEvent.time;

	if (returnEvent.property == None)
		{
		returnEvent.property = returnEvent.target;
		}

	Atom selectionName   = selReqEvent.selection;
	JXDNDManager* dndMgr = itsDisplay->GetDNDManager();
	if (selectionName == kJXClipboardName &&
		dndMgr->IsLastFakePasteTime(selReqEvent.time))
		{
		selectionName = dndMgr->GetDNDSelectionName();
		}

	JXWidget* owner;
	if (GetSelectionOwner(selectionName, selReqEvent.time, &owner))
		{
		Atom returnType;
		unsigned char* data;
		JSize dataLength;
		JSize bitsPerBlock;
		if (owner->MainConvertSelection(selectionName, selReqEvent.target,
										&returnType, &data, &dataLength, &bitsPerBlock))
			{
			#if JXSEL_DEBUG_MSGS && ! JXSEL_DEBUG_ONLY_RESULT
			cout << "Accepted selection request: ";
			cout << XGetAtomName(*itsDisplay, selReqEvent.target);
			cout << ", time=" << selReqEvent.time << endl;
			#endif

			#if JXSEL_MICRO_TRANSFER
			const JSize savedMaxSize = itsMaxDataChunkSize;
			if (selReqEvent.target != itsTargetsXAtom)
				{
				itsMaxDataChunkSize = 1;
				}
			#endif

			SendSelectionData(selReqEvent.requestor,
							  returnEvent.property, returnType,
							  data, dataLength, bitsPerBlock, &xEvent);
			delete [] data;

			#if JXSEL_MICRO_TRANSFER
			itsMaxDataChunkSize = savedMaxSize;
			#endif

			return;
			}
		else
			{
			#if JXSEL_DEBUG_MSGS
			cout << "Rejected selection request: can't convert to ";
			cout << XGetAtomName(*itsDisplay, selReqEvent.target);
			cout << ", time=" << selReqEvent.time << endl;
			#endif
			}
		}
	else
		{
		#if JXSEL_DEBUG_MSGS
		cout << "Rejected selection request: don't own ";
		cout << XGetAtomName(*itsDisplay, selReqEvent.selection);
		cout << ", time=" << selReqEvent.time << endl;
		#endif
		}

	returnEvent.property = None;
	itsDisplay->SendXEvent(selReqEvent.requestor, &xEvent);
}

/******************************************************************************
 SendSelectionData (private)

	Sends the given data either as one chunk or incrementally.

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

void
JXSelectionManager::SendSelectionData
	(
	const Window	requestor,
	const Atom		property,
	const Atom		type,
	unsigned char*	data,
	const JSize		dataLength,
	const JSize		bitsPerBlock,
	XEvent*			returnEvent
	)
{
	JSize chunkSize = 4*itsMaxDataChunkSize;

	// if small enough, send it one chunk

	#if JXSEL_DEBUG_MSGS && ! JXSEL_DEBUG_ONLY_RESULT
	cout << "Selection is " << dataLength << " bytes" << endl;
	if (dataLength <= chunkSize)
		{
		cout << "Attempting to send entire selection" << endl;
		}
	#endif

	if (dataLength <= chunkSize &&
		SendSelectionData1(requestor, property, type,
						   data, dataLength, bitsPerBlock))
		{
		#if JXSEL_DEBUG_MSGS
		cout << "Transfer complete" << endl;
		#endif

		itsDisplay->SendXEvent(requestor, returnEvent);
		return;
		}

	// we need to hear when the property or the window is deleted

	XSelectInput(*itsDisplay, requestor, PropertyChangeMask | StructureNotifyMask);

	// initiate transfer by sending INCR

	#if JXSEL_DEBUG_MSGS && ! JXSEL_DEBUG_ONLY_RESULT
	cout << "Initiating incremental transfer" << endl;
	#endif

	(JXGetApplication())->DisplayBusyCursor();

	XID remainingLength = dataLength;		// must be 32 bits
	XChangeProperty(*itsDisplay, requestor, property, itsIncrementalSendXAtom,
					32, PropModeReplace,
					reinterpret_cast<unsigned char*>(&remainingLength), 4);
	itsDisplay->SendXEvent(requestor, returnEvent);
	if (!WaitForPropertyDeleted(requestor, property))
		{
		#if JXSEL_DEBUG_MSGS
		cout << "No response from requestor (data length)" << endl;
		#endif

		return;
		}

	// send a chunk and wait for it to be deleted

	#if JXSEL_DEBUG_MSGS
	JIndex chunkIndex = 0;
	#endif

	while (remainingLength > 0)
		{
		#if JXSEL_DEBUG_MSGS
		chunkIndex++;
		#endif

		unsigned char* dataStart = data + dataLength - remainingLength;
		if (chunkSize > remainingLength)
			{
			chunkSize = remainingLength;
			}

		#if JXSEL_DEBUG_MSGS && ! JXSEL_DEBUG_ONLY_RESULT
		cout << "Sending " << chunkSize << " bytes" << endl;
		#endif

		SendSelectionData1(requestor, property, type,
						   dataStart, chunkSize, bitsPerBlock);
		if (itsTargetWindowDeletedFlag)
			{
			#if JXSEL_DEBUG_MSGS
			cout << "Requestor crashed on iteration ";
			cout << chunkIndex << endl;
			#endif
			return;
			}
		else if (itsReceivedAllocErrorFlag && itsMaxDataChunkSize > 1)
			{
			itsMaxDataChunkSize /= 2;
			chunkSize            = 4*itsMaxDataChunkSize;

			#if JXSEL_DEBUG_MSGS && ! JXSEL_DEBUG_ONLY_RESULT
			cout << "Reducing chunk size to " << chunkSize << " bytes" << endl;
			#endif
			}
		else if (itsReceivedAllocErrorFlag)
			{
			#if JXSEL_DEBUG_MSGS
			cout << "X server is out of memory!" << endl;
			#endif

			XSelectInput(*itsDisplay, requestor, NoEventMask);
			return;
			}

		if (!WaitForPropertyDeleted(requestor, property))
			{
			#if JXSEL_DEBUG_MSGS
			cout << "No response from requestor on iteration ";
			cout << chunkIndex << ", " << dataLength - remainingLength;
			cout << " bytes sent, " << remainingLength;
			cout << " bytes remaining, chunk size " << chunkSize << endl;
			#endif

			return;
			}

		remainingLength -= chunkSize;
		}

	// send zero-length property to signal that we are done

	SendSelectionData1(requestor, property, type, data, 0, 8);

	// we are done interacting with the requestor

	if (!itsTargetWindowDeletedFlag)
		{
		XSelectInput(*itsDisplay, requestor, NoEventMask);
		}

	#if JXSEL_DEBUG_MSGS
	cout << "Transfer complete" << endl;
	#endif
}

/******************************************************************************
 SendSelectionData1 (private)

	Put the data into the window property and check for BadAlloc and
	BadWindow errors.

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

JBoolean
JXSelectionManager::SendSelectionData1
	(
	const Window	requestor,
	const Atom		property,
	const Atom		type,
	unsigned char*	data,
	const JSize		dataLength,
	const JSize		bitsPerBlock
	)
{
	XChangeProperty(*itsDisplay, requestor, property, type,
					bitsPerBlock, PropModeReplace, data, dataLength);

	itsReceivedAllocErrorFlag  = kFalse;
	itsTargetWindow            = requestor;
	itsTargetWindowDeletedFlag = kFalse;

	itsDisplay->Synchronize();

	ListenTo(itsDisplay);
	itsDisplay->CheckForXErrors();
	StopListening(itsDisplay);

	itsTargetWindow = None;

	return JNegate(itsReceivedAllocErrorFlag || itsTargetWindowDeletedFlag);
}

/******************************************************************************
 WaitForPropertyDeleted (private)

	Wait for the receiver to delete the window property.
	Returns kFalse if we time out or the window is deleted (receiver crash).

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

JBoolean
JXSelectionManager::WaitForPropertyDeleted
	(
	const Window	xWindow,
	const Atom		property
	)
{
	itsDisplay->Synchronize();

	XEvent xEvent;
	XID checkIfEventData[] = { xWindow, property };
	const clock_t endTime = clock() + kWaitForSelectionTime;
	while (clock() < endTime)
		{
		const Bool receivedEvent =
			XCheckIfEvent(*itsDisplay, &xEvent, GetNextPropDeletedEvent,
						  reinterpret_cast<char*>(&checkIfEventData));

		if (receivedEvent && xEvent.type == PropertyNotify)
			{
			return kTrue;
			}
		else if (receivedEvent && xEvent.type == DestroyNotify)
			{
			return kFalse;
			}
		}

	#if JXSEL_DEBUG_MSGS

	Atom actualType;
	int actualFormat;
	unsigned long itemCount, remainingBytes;
	unsigned char* data;
	XGetWindowProperty(*itsDisplay, xWindow, property,
					   0, LONG_MAX, False, AnyPropertyType,
					   &actualType, &actualFormat,
					   &itemCount, &remainingBytes, &data);

	if (actualType == None && actualFormat == 0 && remainingBytes == 0)
		{
		cout << "Window property was deleted, but source was not notified!" << endl;
		}
	else
		{
		cout << "Window property not deleted, type " << XGetAtomName(*itsDisplay, actualType);
		cout << ", " << itemCount * actualFormat/8 << " bytes" << endl;
		}
	XFree(data);

	#endif

	XSelectInput(*itsDisplay, xWindow, NoEventMask);
	return kFalse;
}

// static

Bool
JXSelectionManager::GetNextPropDeletedEvent
	(
	Display*	display,
	XEvent*		event,
	char*		arg
	)
{
	XID* data = reinterpret_cast<XID*>(arg);

	if (event->type             == PropertyNotify &&
		event->xproperty.window == data[0] &&
		event->xproperty.atom   == data[1] &&
		event->xproperty.state  == PropertyDelete)
		{
		return True;
		}
	else if (event->type                  == DestroyNotify &&
			 event->xdestroywindow.window == data[0])
		{
		return True;
		}
	else
		{
		return False;
		}
}

/******************************************************************************
 ReceiveSelectionDataIncr (private)

	Receives the current selection data incrementally.

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

JBoolean
JXSelectionManager::ReceiveSelectionDataIncr
	(
	const Atom		selectionName,
	const Window	xWindow,
	Atom*			returnType,
	unsigned char**	data,
	JSize*			dataLength,
	DeleteMethod*	delMethod
	)
{
	*returnType = None;
	*data       = NULL;
	*dataLength = 0;
	*delMethod  = kCFree;

	#if JXSEL_DEBUG_MSGS && ! JXSEL_DEBUG_ONLY_RESULT
	cout << "Initiating incremental receive" << endl;
	#endif

	(JXGetApplication())->DisplayBusyCursor();

	// we need to hear when the sender crashes

	const Window sender = XGetSelectionOwner(*itsDisplay, selectionName);
	if (sender == None)
		{
		return kFalse;
		}
	XSelectInput(*itsDisplay, sender, StructureNotifyMask);

	// The transfer has already been initiated by deleting the
	// INCR property when it was retrieved.

	// wait for a chunk, retrieve it, and delete it

	#if JXSEL_DEBUG_MSGS
	JIndex chunkIndex = 0;
	#endif

	JBoolean ok = kTrue;
	while (1)
		{
		#if JXSEL_DEBUG_MSGS
		chunkIndex++;
		#endif

		if (!WaitForPropertyCreated(xWindow, itsSelectionWindPropXAtom, sender))
			{
			#if JXSEL_DEBUG_MSGS
			cout << "No response from selection owner on iteration ";
			cout << chunkIndex << ", " << *dataLength << " bytes received" << endl;
			#endif

			ok = kFalse;
			break;
			}

		Atom actualType;
		int actualFormat;
		unsigned long itemCount, remainingBytes;
		unsigned char* chunk;
		XGetWindowProperty(*itsDisplay, xWindow, itsSelectionWindPropXAtom,
						   0, LONG_MAX, True, AnyPropertyType,
						   &actualType, &actualFormat,
						   &itemCount, &remainingBytes, &chunk);

		if (actualType == None)
			{
			#if JXSEL_DEBUG_MSGS
			cout << "Received data of type None on iteration ";
			cout << chunkIndex << endl;
			#endif

			ok = kFalse;
			break;
			}

		// an empty property means that we are done

		if (itemCount == 0)
			{
			#if JXSEL_DEBUG_MSGS
			if (*data == NULL)
				{
				cout << "Didn't receive any data on iteration ";
				cout << chunkIndex << endl;
				}
			#endif

			XFree(chunk);
			ok = JConvertToBoolean( *data != NULL );
			break;
			}

		// otherwise, append it to *data

		else
			{
			assert( remainingBytes == 0 );

			const JSize chunkSize = itemCount * actualFormat/8;
			if (*data == NULL)
				{
				// the first chunk determines the format
				*returnType = actualType;

				*data = static_cast<unsigned char*>(malloc(chunkSize));
				assert( *data != NULL );
				memcpy(*data, chunk, chunkSize);

				#if JXSEL_DEBUG_MSGS && ! JXSEL_DEBUG_ONLY_RESULT
				cout << "Data format: " << XGetAtomName(*itsDisplay, actualType) << endl;
				#endif
				}
			else
				{
				*data = static_cast<unsigned char*>(realloc(*data, *dataLength + chunkSize));
				memcpy(*data + *dataLength, chunk, chunkSize);
				}

			*dataLength += chunkSize;
			XFree(chunk);

			#if JXSEL_DEBUG_MSGS && ! JXSEL_DEBUG_ONLY_RESULT
			cout << "Received " << chunkSize << " bytes" << endl;
			#endif
			}
		}

	// we are done interacting with the sender

	XSelectInput(*itsDisplay, sender, NoEventMask);

	// clean up

	if (!ok && *data != NULL)
		{
		free(*data);
		*data       = NULL;
		*dataLength = 0;
		*returnType = None;
		}

	#if JXSEL_DEBUG_MSGS
	if (ok)
		{
		cout << "Transfer successful" << endl;
		}
	else
		{
		cout << "Transfer failed" << endl;
		}
	#endif

	return ok;
}

/******************************************************************************
 WaitForPropertyCreated (private)

	Wait for the receiver to create the window property.
	Returns kFalse if we time out or the sender crashes.

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

JBoolean
JXSelectionManager::WaitForPropertyCreated
	(
	const Window	xWindow,
	const Atom		property,
	const Window	sender
	)
{
	itsDisplay->Synchronize();

	XEvent xEvent;
	XID checkIfEventData[] = { xWindow, property };
	const clock_t endTime = clock() + kWaitForSelectionTime;
	while (clock() < endTime)
		{
		if (XCheckTypedWindowEvent(*itsDisplay, sender, DestroyNotify, &xEvent))
			{
			return kFalse;
			}

		if (XCheckIfEvent(*itsDisplay, &xEvent, GetNextNewPropertyEvent,
						  reinterpret_cast<char*>(checkIfEventData)))
			{
			return kTrue;
			}
		}

	return kFalse;
}

// static

Bool
JXSelectionManager::GetNextNewPropertyEvent
	(
	Display*	display,
	XEvent*		event,
	char*		arg
	)
{
	XID* data = reinterpret_cast<XID*>(arg);

	if (event->type             == PropertyNotify &&
		event->xproperty.window == data[0] &&
		event->xproperty.atom   == data[1] &&
		event->xproperty.state  == PropertyNewValue)
		{
		return True;
		}
	else
		{
		return False;
		}
}

/******************************************************************************
 ReceiveWithFeedback (virtual protected)

	This catches errors while sending and receiving data.

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

void
JXSelectionManager::ReceiveWithFeedback
	(
	JBroadcaster*	sender,
	Message*		message
	)
{
	if (sender == itsDisplay && message->Is(JXDisplay::kXError))
		{
		JXDisplay::XError* err = dynamic_cast(JXDisplay::XError*, message);
		assert( err != NULL );

		if (err->GetType() == BadAlloc)
			{
			itsReceivedAllocErrorFlag = kTrue;
			err->SetCaught();
			}

		else if (err->GetType() == BadWindow &&
				 err->GetXID()  == itsTargetWindow)
			{
			itsTargetWindowDeletedFlag = kTrue;
			err->SetCaught();
			}
		}

	else
		{
		JBroadcaster::ReceiveWithFeedback(sender, message);
		}
}

/******************************************************************************
 GetSelectionOwner

	time can be CurrentTime.

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

JBoolean
JXSelectionManager::GetSelectionOwner
	(
	const Atom	selectionName,
	const Time	time,
	JXWidget**	owner
	)
{
	OwnerInfo info(selectionName, NULL, None, 0);
	JIndex ownerIndex;
	if (itsOwnerList->SearchSorted(info, JOrderedSetT::kAnyMatch, &ownerIndex))
		{
		info = itsOwnerList->GetElement(ownerIndex);
		if (time == CurrentTime || time >= info.startTime)
			{
			*owner = info.owner;
			return JConvertToBoolean( *owner != NULL );
			}
		}

	*owner = NULL;
	return kFalse;
}

/******************************************************************************
 BecomeOwner (private)

	Called by JXWidget.

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

JBoolean
JXSelectionManager::BecomeOwner
	(
	JXWidget*	widget,
	const Atom	selectionName
	)
{
	const Time lastEventTime = itsDisplay->GetLastEventTime();

	// check if it already owns the selection

	OwnerInfo info(selectionName, NULL, None, 0);
	JBoolean found;
	const JIndex ownerIndex =
		itsOwnerList->SearchSorted1(info, JOrderedSetT::kAnyMatch, &found);
	if (found)
		{
		info = itsOwnerList->GetElement(ownerIndex);
		if (info.owner == widget)
			{
			#if JXSEL_DEBUG_MSGS
			cout << "Got selection ownership: ";
			cout << XGetAtomName(*itsDisplay, selectionName);
			cout << ", time=" << lastEventTime << endl;
			#endif

			info.startTime = lastEventTime;
			itsOwnerList->SetElement(ownerIndex, info);
			return kTrue;
			}
		}
	else
		{
		// We have never seen this selection name before so we add
		// it to our list.

		itsOwnerList->InsertElementAtIndex(ownerIndex, info);
		}

	// At this point, info contains the current information about the
	// given selection and ownerIndex points to info in the list.

	const Window xWindow = (widget->GetWindow())->GetXWindow();
	XSetSelectionOwner(*itsDisplay, selectionName, xWindow, lastEventTime);

	// We are required to check XGetSelectionOwner()

	const Window w = XGetSelectionOwner(*itsDisplay, selectionName);

	if (w == xWindow)
		{
		#if JXSEL_DEBUG_MSGS
		cout << "Got selection ownership: ";
		cout << XGetAtomName(*itsDisplay, selectionName);
		cout << ", time=" << lastEventTime << endl;
		#endif

		if (info.owner != NULL)
			{
			(info.owner)->PrivateLostSelectionOwnership(info.name);
			}
		info.owner     = widget;
		info.xWindow   = xWindow;
		info.startTime = lastEventTime;
		itsOwnerList->SetElement(ownerIndex, info);

		(info.owner)->GotSelectionOwnership(info.name, lastEventTime);
		return kTrue;
		}
	else
		{
		return kFalse;
		}
}

/******************************************************************************
 RelinquishOwnership (private)

	Called by JXWidget.

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

void
JXSelectionManager::RelinquishOwnership
	(
	JXWidget*	widget,
	const Atom	selectionName
	)
{
#ifndef NDEBUG
	{
	JXWidget* owner;
	const JBoolean found = GetSelectionOwner(selectionName, CurrentTime, &owner);
	assert( found && widget == owner );
	}
#endif

	ClearSelectionOwner(selectionName);
}

/******************************************************************************
 ClearSelectionOwner

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

void
JXSelectionManager::ClearSelectionOwner
	(
	const Atom selectionName
	)
{
	OwnerInfo info(selectionName, NULL, None, 0);
	JIndex ownerIndex;
	if (itsOwnerList->SearchSorted(info, JOrderedSetT::kAnyMatch, &ownerIndex))
		{
		info = itsOwnerList->GetElement(ownerIndex);
		if (info.owner != NULL)
			{
			(info.owner)->PrivateLostSelectionOwnership(selectionName);

			if (XGetSelectionOwner(*itsDisplay, selectionName) == info.xWindow)
				{
				XSetSelectionOwner(*itsDisplay, selectionName, None,
								   itsDisplay->GetLastEventTime());
				}

			info.owner     = NULL;
			info.xWindow   = None;
			info.startTime = 0;
			itsOwnerList->SetElement(ownerIndex, info);
			}
		}
}

/******************************************************************************
 CompareSelectionNames (static private)

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

JOrderedSetT::CompareResult
JXSelectionManager::CompareSelectionNames
	(
	const OwnerInfo& info1,
	const OwnerInfo& info2
	)
{
	if (info1.name < info2.name)
		{
		return JOrderedSetT::kFirstLessSecond;
		}
	else if (info1.name == info2.name)
		{
		return JOrderedSetT::kFirstEqualSecond;
		}
	else
		{
		return JOrderedSetT::kFirstGreaterSecond;
		}
}

#define JTemplateType JXSelectionManager::OwnerInfo
#include <JArray.tmpls>
#undef JTemplateType
