/*
 * synergy -- mouse and keyboard sharing utility
 * Copyright (C) 2002 Chris Schoeneman
 * 
 * This package is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * found in the file COPYING that should have accompanied this file.
 * 
 * This package is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 */

#include "CXWindowsClipboard.h"
#include "CXWindowsClipboardTextConverter.h"
#include "CXWindowsClipboardUCS2Converter.h"
#include "CXWindowsClipboardUTF8Converter.h"
#include "CXWindowsClipboardHTMLConverter.h"
#include "CXWindowsClipboardBMPConverter.h"
#include "CXWindowsUtil.h"
#include "CThread.h"
#include "CLog.h"
#include "CStopwatch.h"
#include "CArch.h"
#include "stdvector.h"
#include <cstdio>
#include <X11/Xatom.h>

//
// CXWindowsClipboard
//

CXWindowsClipboard::CXWindowsClipboard(Display* display,
				Window window, ClipboardID id) :
	m_display(display),
	m_window(window),
	m_id(id),
	m_open(false),
	m_time(0),
	m_owner(false),
	m_timeOwned(0),
	m_timeLost(0)
{
	// get some atoms
	m_atomTargets         = XInternAtom(m_display, "TARGETS", False);
	m_atomMultiple        = XInternAtom(m_display, "MULTIPLE", False);
	m_atomTimestamp       = XInternAtom(m_display, "TIMESTAMP", False);
	m_atomInteger         = XInternAtom(m_display, "INTEGER", False);
	m_atomAtom            = XInternAtom(m_display, "ATOM", False);
	m_atomAtomPair        = XInternAtom(m_display, "ATOM_PAIR", False);
	m_atomData            = XInternAtom(m_display, "CLIP_TEMPORARY", False);
	m_atomINCR            = XInternAtom(m_display, "INCR", False);
	m_atomMotifClipLock   = XInternAtom(m_display, "_MOTIF_CLIP_LOCK", False);
	m_atomMotifClipHeader = XInternAtom(m_display, "_MOTIF_CLIP_HEADER", False);
	m_atomMotifClipAccess = XInternAtom(m_display,
								"_MOTIF_CLIP_LOCK_ACCESS_VALID", False);
	m_atomGDKSelection    = XInternAtom(m_display, "GDK_SELECTION", False);

	// set selection atom based on clipboard id
	switch (id) {
	case kClipboardClipboard:
		m_selection = XInternAtom(m_display, "CLIPBOARD", False);
		break;

	case kClipboardSelection:
	default:
		m_selection = XA_PRIMARY;
		break;
	}

	// add converters, most desired first
	m_converters.push_back(new CXWindowsClipboardHTMLConverter(m_display,
								"text/html"));
	m_converters.push_back(new CXWindowsClipboardBMPConverter(m_display));
	m_converters.push_back(new CXWindowsClipboardUTF8Converter(m_display,
								"text/plain;charset=UTF-8"));
	m_converters.push_back(new CXWindowsClipboardUTF8Converter(m_display,
								"UTF8_STRING"));
	m_converters.push_back(new CXWindowsClipboardUCS2Converter(m_display,
								"text/plain;charset=ISO-10646-UCS-2"));
	m_converters.push_back(new CXWindowsClipboardUCS2Converter(m_display,
								"text/unicode"));
	m_converters.push_back(new CXWindowsClipboardTextConverter(m_display,
								"text/plain"));
	m_converters.push_back(new CXWindowsClipboardTextConverter(m_display,
								"STRING"));

	// we have no data
	clearCache();
}

CXWindowsClipboard::~CXWindowsClipboard()
{
	clearReplies();
	clearConverters();
}

void
CXWindowsClipboard::lost(Time time)
{
	LOG((CLOG_DEBUG "lost clipboard %d ownership at %d", m_id, time));
	if (m_owner) {
		m_owner    = false;
		m_timeLost = time;
		clearCache();
	}
}

void
CXWindowsClipboard::addRequest(Window owner, Window requestor,
				Atom target, ::Time time, Atom property)
{
	// must be for our window and we must have owned the selection
	// at the given time.
	bool success = false;
	if (owner == m_window) {
		LOG((CLOG_DEBUG1 "request for clipboard %d, target %s by 0x%08x (property=%s)", m_selection, CXWindowsUtil::atomToString(m_display, target).c_str(), requestor, CXWindowsUtil::atomToString(m_display, property).c_str()));
		if (wasOwnedAtTime(time)) {
			if (target == m_atomMultiple) {
				// add a multiple request.  property may not be None
				// according to ICCCM.
				if (property != None) {
					success = insertMultipleReply(requestor, time, property);
				}
			}
			else {
				addSimpleRequest(requestor, target, time, property);

				// addSimpleRequest() will have already handled failure
				success = true;
			}
		}
		else {
			LOG((CLOG_DEBUG1 "failed, not owned at time %d", time));
		}
	}

	if (!success) {
		// send failure
		LOG((CLOG_DEBUG1 "failed"));
		insertReply(new CReply(requestor, target, time));
	}

	// send notifications that are pending
	pushReplies();
}

bool
CXWindowsClipboard::addSimpleRequest(Window requestor,
				Atom target, ::Time time, Atom property)
{
	// obsolete requestors may supply a None property.  in
	// that case we use the target as the property to store
	// the conversion.
	if (property == None) {
		property = target;
	}

	// handle targets
	CString data;
	Atom type  = None;
	int format = 0;
	if (target == m_atomTargets) {
		type = getTargetsData(data, &format);
	}
	else if (target == m_atomTimestamp) {
		type = getTimestampData(data, &format);
	}
	else {
		IXWindowsClipboardConverter* converter = getConverter(target);
		if (converter != NULL) {
			IClipboard::EFormat clipboardFormat = converter->getFormat();
			if (m_added[clipboardFormat]) {
				try {
					data   = converter->fromIClipboard(m_data[clipboardFormat]);
					format = converter->getDataSize();
					type   = converter->getAtom();
				}
				catch (...) {
					// ignore -- cannot convert
				}
			}
		}
	}

	if (type != None) {
		// success
		LOG((CLOG_DEBUG1 "success"));
		insertReply(new CReply(requestor, target, time,
								property, data, type, format));
		return true;
	}
	else {
		// failure
		LOG((CLOG_DEBUG1 "failed"));
		insertReply(new CReply(requestor, target, time));
		return false;
	}
}

bool
CXWindowsClipboard::processRequest(Window requestor,
				::Time /*time*/, Atom property)
{
	CReplyMap::iterator index = m_replies.find(requestor);
	if (index == m_replies.end()) {
		// unknown requestor window
		return false;
	}
	LOG((CLOG_DEBUG1 "received property %s delete from 0x08%x", CXWindowsUtil::atomToString(m_display, property).c_str(), requestor));

	// find the property in the known requests.  it should be the
	// first property but we'll check 'em all if we have to.
	CReplyList& replies = index->second;
	for (CReplyList::iterator index2 = replies.begin();
								index2 != replies.end(); ++index2) {
		CReply* reply = *index2;
		if (reply->m_replied && reply->m_property == property) {
			// if reply is complete then remove it and start the
			// next one.
			pushReplies(index, replies, index2);
			return true;
		}
	}

	return false;
}

bool
CXWindowsClipboard::destroyRequest(Window requestor)
{
	CReplyMap::iterator index = m_replies.find(requestor);
	if (index == m_replies.end()) {
		// unknown requestor window
		return false;
	}

	// destroy all replies for this window
	clearReplies(index->second);
	m_replies.erase(index);

	// note -- we don't stop watching the window for events because
	// we're called in response to the window being destroyed.

	return true;
}

Window
CXWindowsClipboard::getWindow() const
{
	return m_window;
}

Atom
CXWindowsClipboard::getSelection() const
{
	return m_selection;
}

bool
CXWindowsClipboard::empty()
{
	assert(m_open);

	LOG((CLOG_DEBUG "empty clipboard %d", m_id));

	// assert ownership of clipboard
	XSetSelectionOwner(m_display, m_selection, m_window, m_time);
	if (XGetSelectionOwner(m_display, m_selection) != m_window) {
		LOG((CLOG_DEBUG "failed to grab clipboard %d", m_id));
		return false;
	}

	// clear all data.  since we own the data now, the cache is up
	// to date.
	clearCache();
	m_cached = true;

	// FIXME -- actually delete motif clipboard items?
	// FIXME -- do anything to motif clipboard properties?

	// save time
	m_timeOwned = m_time;
	m_timeLost  = 0;

	// we're the owner now
	m_owner = true;
	LOG((CLOG_DEBUG "grabbed clipboard %d", m_id));

	return true;
}

void
CXWindowsClipboard::add(EFormat format, const CString& data)
{
	assert(m_open);
	assert(m_owner);

	LOG((CLOG_DEBUG "add %d bytes to clipboard %d format: %d", data.size(), m_id, format));

	m_data[format]  = data;
	m_added[format] = true;

	// FIXME -- set motif clipboard item?
}

bool
CXWindowsClipboard::open(Time time) const
{
	assert(!m_open);

	LOG((CLOG_DEBUG "open clipboard %d", m_id));

	// assume not motif
	m_motif = false;

	// lock clipboard
	if (m_id == kClipboardClipboard) {
		if (!motifLockClipboard()) {
			return false;
		}

		// check if motif owns the selection.  unlock motif clipboard
		// if it does not.
		m_motif = motifOwnsClipboard();
		LOG((CLOG_DEBUG1 "motif does %sown clipboard", m_motif ? "" : "not "));
		if (!m_motif) {
			motifUnlockClipboard();
		}
	}

	// now open
	m_open = true;
	m_time = time;

	// be sure to flush the cache later if it's dirty
	m_checkCache = true;

	return true;
}

void
CXWindowsClipboard::close() const
{
	assert(m_open);

	LOG((CLOG_DEBUG "close clipboard %d", m_id));

	// unlock clipboard
	if (m_motif) {
		motifUnlockClipboard();
	}

	m_motif = false;
	m_open  = false;
}

IClipboard::Time
CXWindowsClipboard::getTime() const
{
	checkCache();
	return m_timeOwned;
}

bool
CXWindowsClipboard::has(EFormat format) const
{
	assert(m_open);

	fillCache();
	return m_added[format];
}

CString
CXWindowsClipboard::get(EFormat format) const
{
	assert(m_open);

	fillCache();
	return m_data[format];
}

void
CXWindowsClipboard::clearConverters()
{
	for (ConverterList::iterator index = m_converters.begin();
								index != m_converters.end(); ++index) {
		delete *index;
	}
	m_converters.clear();
}

IXWindowsClipboardConverter*
CXWindowsClipboard::getConverter(Atom target, bool onlyIfNotAdded) const
{
	IXWindowsClipboardConverter* converter = NULL;
	for (ConverterList::const_iterator index = m_converters.begin();
								index != m_converters.end(); ++index) {
		converter = *index;
		if (converter->getAtom() == target) {
			break;
		}
	}
	if (converter == NULL) {
		LOG((CLOG_DEBUG1 "  no converter for target %s", CXWindowsUtil::atomToString(m_display, target).c_str()));
		return NULL;
	}

	// optionally skip already handled targets
	if (onlyIfNotAdded) {
		if (m_added[converter->getFormat()]) {
			LOG((CLOG_DEBUG1 "  skipping handled format %d", converter->getFormat()));
			return NULL;
		}
	}

	return converter;
}

void
CXWindowsClipboard::checkCache() const
{
	if (!m_checkCache) {
		return;
	}
	m_checkCache = false;

	// get the time the clipboard ownership was taken by the current
	// owner.
	if (m_motif) {
		m_timeOwned = motifGetTime();
	}
	else {
		m_timeOwned = icccmGetTime();
	}

	// if we can't get the time then use the time passed to us
	if (m_timeOwned == 0) {
		m_timeOwned = m_time;
	}

	// if the cache is dirty then flush it
	if (m_timeOwned != m_cacheTime) {
		clearCache();
	}
}

void
CXWindowsClipboard::clearCache() const
{
	const_cast<CXWindowsClipboard*>(this)->doClearCache();
}

void
CXWindowsClipboard::doClearCache()
{
	m_checkCache = false;
	m_cached     = false;
	for (SInt32 index = 0; index < kNumFormats; ++index) {
		m_data[index]  = "";
		m_added[index] = false;
	}
}

void
CXWindowsClipboard::fillCache() const
{
	// get the selection data if not already cached
	checkCache();
	if (!m_cached) {
		const_cast<CXWindowsClipboard*>(this)->doFillCache();
	}
}

void
CXWindowsClipboard::doFillCache()
{
	if (m_motif) {
		motifFillCache();
	}
	else {
		icccmFillCache();
	}
	m_checkCache = false;
	m_cached     = true;
	m_cacheTime  = m_timeOwned;
}

void
CXWindowsClipboard::icccmFillCache()
{
	LOG((CLOG_DEBUG "ICCCM fill clipboard %d", m_id));

	// see if we can get the list of available formats from the selection.
	// if not then use a default list of formats.  note that some clipboard
	// owners are broken and report TARGETS as the type of the TARGETS data
	// instead of the correct type ATOM;  allow either.
	const Atom atomTargets = m_atomTargets;
	Atom target;
	CString data;
	if (!icccmGetSelection(atomTargets, &target, &data) ||
		(target != m_atomAtom && target != m_atomTargets)) {
		LOG((CLOG_DEBUG1 "selection doesn't support TARGETS"));
		data = "";

		target = XA_STRING;
		data.append(reinterpret_cast<char*>(&target), sizeof(target));
	}

	const Atom* targets = reinterpret_cast<const Atom*>(data.data());
	const UInt32 numTargets = data.size() / sizeof(Atom);
	LOG((CLOG_DEBUG "  available targets: %s", CXWindowsUtil::atomsToString(m_display, targets, numTargets).c_str()));

	// try each converter in order (because they're in order of
	// preference).
	for (ConverterList::const_iterator index = m_converters.begin();
								index != m_converters.end(); ++index) {
		IXWindowsClipboardConverter* converter = *index;

		// skip already handled targets
		if (m_added[converter->getFormat()]) {
			continue;
		}

		// see if atom is in target list
		Atom target = None;
		for (UInt32 i = 0; i < numTargets; ++i) {
			if (converter->getAtom() == targets[i]) {
				target = targets[i];
				break;
			}
		}
		if (target == None) {
			continue;
		}

		// get the data
		Atom actualTarget;
		CString targetData;
		if (!icccmGetSelection(target, &actualTarget, &targetData)) {
			LOG((CLOG_DEBUG1 "  no data for target %s", CXWindowsUtil::atomToString(m_display, target).c_str()));
			continue;
		}

		// add to clipboard and note we've done it
		IClipboard::EFormat format = converter->getFormat();
		m_data[format]  = converter->toIClipboard(targetData);
		m_added[format] = true;
		LOG((CLOG_DEBUG "  added format %d for target %s (%u %s)", format, CXWindowsUtil::atomToString(m_display, target).c_str(), targetData.size(), targetData.size() == 1 ? "byte" : "bytes"));
	}
}

bool
CXWindowsClipboard::icccmGetSelection(Atom target,
				Atom* actualTarget, CString* data) const
{
	assert(actualTarget != NULL);
	assert(data         != NULL);

	// request data conversion
	CICCCMGetClipboard getter(m_window, m_time, m_atomData);
	if (!getter.readClipboard(m_display, m_selection,
								target, actualTarget, data)) {
		LOG((CLOG_DEBUG1 "can't get data for selection target %s", CXWindowsUtil::atomToString(m_display, target).c_str()));
		LOGC(getter.m_error, (CLOG_WARN "ICCCM violation by clipboard owner"));
		return false;
	}
	else if (*actualTarget == None) {
		LOG((CLOG_DEBUG1 "selection conversion failed for target %s", CXWindowsUtil::atomToString(m_display, target).c_str()));
		return false;
	}
	return true;
}

IClipboard::Time
CXWindowsClipboard::icccmGetTime() const
{
	Atom actualTarget;
	CString data;
	if (icccmGetSelection(m_atomTimestamp, &actualTarget, &data) &&
		actualTarget == m_atomInteger) {
		Time time = *reinterpret_cast<const Time*>(data.data());
		LOG((CLOG_DEBUG1 "got ICCCM time %d", time));
		return time;
	}
	else {
		// no timestamp
		LOG((CLOG_DEBUG1 "can't get ICCCM time"));
		return 0;
	}
}

bool
CXWindowsClipboard::motifLockClipboard() const
{
	// fail if anybody owns the lock (even us, so this is non-recursive)
    Window lockOwner = XGetSelectionOwner(m_display, m_atomMotifClipLock);
	if (lockOwner != None) {
		LOG((CLOG_DEBUG1 "motif lock owner 0x%08x", lockOwner));
		return false;
	}

	// try to grab the lock
	// FIXME -- is this right?  there's a race condition here --
	// A grabs successfully, B grabs successfully, A thinks it
	// still has the grab until it gets a SelectionClear.
	Time time = CXWindowsUtil::getCurrentTime(m_display, m_window);
	XSetSelectionOwner(m_display, m_atomMotifClipLock, m_window, time);
    lockOwner = XGetSelectionOwner(m_display, m_atomMotifClipLock);
	if (lockOwner != m_window) {
		LOG((CLOG_DEBUG1 "motif lock owner 0x%08x", lockOwner));
		return false;
	}

	LOG((CLOG_DEBUG1 "locked motif clipboard"));
	return true;
}

void
CXWindowsClipboard::motifUnlockClipboard() const
{
	LOG((CLOG_DEBUG1 "unlocked motif clipboard"));

	// fail if we don't own the lock
	Window lockOwner = XGetSelectionOwner(m_display, m_atomMotifClipLock);
	if (lockOwner != m_window) {
		return;
	}

	// release lock
	Time time = CXWindowsUtil::getCurrentTime(m_display, m_window);
	XSetSelectionOwner(m_display, m_atomMotifClipLock, None, time);
}

bool
CXWindowsClipboard::motifOwnsClipboard() const
{
	// get the current selection owner
	// FIXME -- this can't be right.  even if the window is destroyed
	// Motif will still have a valid clipboard.  how can we tell if
	// some other client owns CLIPBOARD?
	Window owner = XGetSelectionOwner(m_display, m_selection);
	if (owner == None) {
		return false;
	}

	// get the Motif clipboard header property from the root window
	Atom target;
	SInt32 format;
	CString data;
	Window root = RootWindow(m_display, DefaultScreen(m_display));
	if (!CXWindowsUtil::getWindowProperty(m_display, root,
								m_atomMotifClipHeader,
								&data, &target, &format, False)) {
		return false;
	}

	// check the owner window against the current clipboard owner
	const CMotifClipHeader* header =
						reinterpret_cast<const CMotifClipHeader*>(data.data());
	if (data.size() >= sizeof(CMotifClipHeader) &&
		header->m_id == kMotifClipHeader) {
		if (header->m_selectionOwner == owner) {
			return true;
		}
	}

	return false;
}

void
CXWindowsClipboard::motifFillCache()
{
	LOG((CLOG_DEBUG "Motif fill clipboard %d", m_id));

	// get the Motif clipboard header property from the root window
	Atom target;
	SInt32 format;
	CString data;
	Window root = RootWindow(m_display, DefaultScreen(m_display));
	if (!CXWindowsUtil::getWindowProperty(m_display, root,
								m_atomMotifClipHeader,
								&data, &target, &format, False)) {
		return;
	}

	// check that the header is okay
	const CMotifClipHeader* header =
						reinterpret_cast<const CMotifClipHeader*>(data.data());
	if (data.size() < sizeof(CMotifClipHeader) ||
		header->m_id != kMotifClipHeader ||
		header->m_numItems < 1) {
		return;
	}

	// get the Motif item property from the root window
	char name[18 + 20];
	sprintf(name, "_MOTIF_CLIP_ITEM_%d", header->m_item);
    Atom atomItem = XInternAtom(m_display, name, False);
	data = "";
	if (!CXWindowsUtil::getWindowProperty(m_display, root,
								atomItem, &data,
								&target, &format, False)) {
		return;
	}

	// check that the item is okay
	const CMotifClipItem* item =
					reinterpret_cast<const CMotifClipItem*>(data.data());
	if (data.size() < sizeof(CMotifClipItem) ||
		item->m_id != kMotifClipItem ||
		item->m_numFormats - item->m_numDeletedFormats < 1) {
		return;
	}

	// format list is after static item structure elements
	const SInt32 numFormats = item->m_numFormats - item->m_numDeletedFormats;
	const SInt32* formats   = reinterpret_cast<const SInt32*>(item->m_size +
								reinterpret_cast<const char*>(data.data()));

	// get the available formats
	typedef std::map<Atom, CString> CMotifFormatMap;
	CMotifFormatMap motifFormats;
	for (SInt32 i = 0; i < numFormats; ++i) {
		// get Motif format property from the root window
		sprintf(name, "_MOTIF_CLIP_ITEM_%d", formats[i]);
    	Atom atomFormat = XInternAtom(m_display, name, False);
		CString data;
		if (!CXWindowsUtil::getWindowProperty(m_display, root,
									atomFormat, &data,
									&target, &format, False)) {
			continue;
		}

		// check that the format is okay
		const CMotifClipFormat* motifFormat =
						reinterpret_cast<const CMotifClipFormat*>(data.data());
		if (data.size() < sizeof(CMotifClipFormat) ||
			motifFormat->m_id != kMotifClipFormat ||
			motifFormat->m_length < 0 ||
			motifFormat->m_type == None ||
			motifFormat->m_deleted != 0) {
			continue;
		}

		// save it
		motifFormats.insert(std::make_pair(motifFormat->m_type, data));
	}
	//const UInt32 numMotifFormats = motifFormats.size();

	// try each converter in order (because they're in order of
	// preference).
	for (ConverterList::const_iterator index = m_converters.begin();
								index != m_converters.end(); ++index) {
		IXWindowsClipboardConverter* converter = *index;

		// skip already handled targets
		if (m_added[converter->getFormat()]) {
			continue;
		}

		// see if atom is in target list
		CMotifFormatMap::const_iterator index2 =
								motifFormats.find(converter->getAtom());
		if (index2 == motifFormats.end()) {
			continue;
		}

		// get format
		const CMotifClipFormat* motifFormat =
								reinterpret_cast<const CMotifClipFormat*>(
									index2->second.data());
		const Atom target                   = motifFormat->m_type;

		// get the data (finally)
		Atom actualTarget;
		CString targetData;
		if (!motifGetSelection(motifFormat, &actualTarget, &targetData)) {
			LOG((CLOG_DEBUG1 "  no data for target %s", CXWindowsUtil::atomToString(m_display, target).c_str()));
			continue;
		}

		// add to clipboard and note we've done it
		IClipboard::EFormat format = converter->getFormat();
		m_data[format]  = converter->toIClipboard(targetData);
		m_added[format] = true;
		LOG((CLOG_DEBUG "  added format %d for target %s", format, CXWindowsUtil::atomToString(m_display, target).c_str()));
	}
}

bool
CXWindowsClipboard::motifGetSelection(const CMotifClipFormat* format,
							Atom* actualTarget, CString* data) const
{
	// if the current clipboard owner and the owner indicated by the
	// motif clip header are the same then transfer via a property on
	// the root window, otherwise transfer as a normal ICCCM client.
	if (!motifOwnsClipboard()) {
		return icccmGetSelection(format->m_type, actualTarget, data);
	}

	// use motif way
	// FIXME -- this isn't right.  it'll only work if the data is
	// already stored on the root window and only if it fits in a
	// property.  motif has some scheme for transferring part by
	// part that i don't know.
	char name[18 + 20];
	sprintf(name, "_MOTIF_CLIP_ITEM_%d", format->m_data);
   	Atom target = XInternAtom(m_display, name, False);
	Window root = RootWindow(m_display, DefaultScreen(m_display));
	return CXWindowsUtil::getWindowProperty(m_display, root,
								target, data,
								actualTarget, NULL, False);
}

IClipboard::Time
CXWindowsClipboard::motifGetTime() const
{
	return icccmGetTime();
}

bool
CXWindowsClipboard::insertMultipleReply(Window requestor,
				::Time time, Atom property)
{
	// get the requested targets
	Atom target;
	SInt32 format;
	CString data;
	if (!CXWindowsUtil::getWindowProperty(m_display, requestor,
								property, &data, &target, &format, False)) {
		// can't get the requested targets
		return false;
	}

	// fail if the requested targets isn't of the correct form
	if (format != 32 ||
		target != m_atomAtomPair) {
		return false;
	}

	// data is a list of atom pairs:  target, property
	const Atom* targets = reinterpret_cast<const Atom*>(data.data());
	const UInt32 numTargets = data.size() / sizeof(Atom);

	// add replies for each target
	bool changed = false;
	for (UInt32 i = 0; i < numTargets; i += 2) {
		const Atom target   = targets[i + 0];
		const Atom property = targets[i + 1];
		if (!addSimpleRequest(requestor, target, time, property)) {
			// note that we can't perform the requested conversion
			static const Atom none = None;
			data.replace(i * sizeof(Atom), sizeof(Atom),
								reinterpret_cast<const char*>(&none),
								sizeof(Atom));
			changed = true;
		}
	}

	// update the targets property if we changed it
	if (changed) {
		CXWindowsUtil::setWindowProperty(m_display, requestor,
								property, data.data(), data.size(),
								target, format);
	}

	// add reply for MULTIPLE request
	insertReply(new CReply(requestor, m_atomMultiple,
								time, property, CString(), None, 32));

	return true;
}

void
CXWindowsClipboard::insertReply(CReply* reply)
{
	assert(reply != NULL);

	// note -- we must respond to requests in order if requestor,target,time
	// are the same, otherwise we can use whatever order we like with one
	// exception:  each reply in a MULTIPLE reply must be handled in order
	// as well.  those replies will almost certainly not share targets so
	// we can't simply use requestor,target,time as map index.
	//
	// instead we'll use just the requestor.  that's more restrictive than
	// necessary but we're guaranteed to do things in the right order.
	// note that we could also include the time in the map index and still
	// ensure the right order.  but since that'll just make it harder to
	// find the right reply when handling property notify events we stick
	// to just the requestor.

	const bool newWindow = (m_replies.count(reply->m_requestor) == 0);
	m_replies[reply->m_requestor].push_back(reply);

	// adjust requestor's event mask if we haven't done so already.  we
	// want events in case the window is destroyed or any of its
	// properties change.
	if (newWindow) {
		// note errors while we adjust event masks
		bool error = false;
		CXWindowsUtil::CErrorLock lock(m_display, &error);

		// get and save the current event mask
		XWindowAttributes attr;
		XGetWindowAttributes(m_display, reply->m_requestor, &attr);
		m_eventMasks[reply->m_requestor] = attr.your_event_mask;

		// add the events we want
		XSelectInput(m_display, reply->m_requestor, attr.your_event_mask |
								StructureNotifyMask | PropertyChangeMask);

		// if we failed then the window has already been destroyed
		if (error) {
			m_replies.erase(reply->m_requestor);
			delete reply;
		}
	}
}

void
CXWindowsClipboard::pushReplies()
{
	// send the first reply for each window if that reply hasn't
	// been sent yet.
	for (CReplyMap::iterator index = m_replies.begin();
								index != m_replies.end(); ++index) {
		assert(!index->second.empty());
		if (!index->second.front()->m_replied) {
			pushReplies(index, index->second, index->second.begin());
		}
	}
}

void
CXWindowsClipboard::pushReplies(CReplyMap::iterator mapIndex,
				CReplyList& replies, CReplyList::iterator index)
{
	CReply* reply = *index;
	while (sendReply(reply)) {
		// reply is complete.  discard it and send the next reply,
		// if any.
		index = replies.erase(index);
		delete reply;
		if (index == replies.end()) {
			break;
		}
		reply = *index;
	}

	// if there are no more replies in the list then remove the list
	// and stop watching the requestor for events.
	if (replies.empty()) {
		CXWindowsUtil::CErrorLock lock(m_display);
		Window requestor = mapIndex->first;
		XSelectInput(m_display, requestor, m_eventMasks[requestor]);
		m_replies.erase(mapIndex);
		m_eventMasks.erase(requestor);
	}
}

bool
CXWindowsClipboard::sendReply(CReply* reply)
{
	assert(reply != NULL);

	// bail out immediately if reply is done
	if (reply->m_done) {
		LOG((CLOG_DEBUG1 "clipboard: finished reply to 0x%08x,%d,%d", reply->m_requestor, reply->m_target, reply->m_property));
		return true;
	}

	// start in failed state if property is None
	bool failed = (reply->m_property == None);
	if (!failed) {
		LOG((CLOG_DEBUG1 "clipboard: setting property on 0x%08x,%d,%d", reply->m_requestor, reply->m_target, reply->m_property));

		// send using INCR if already sending incrementally or if reply
		// is too large, otherwise just send it.
		const UInt32 maxRequestSize = 3 * XMaxRequestSize(m_display);
		const bool useINCR = (reply->m_data.size() > maxRequestSize);

		// send INCR reply if incremental and we haven't replied yet
		if (useINCR && !reply->m_replied) {
			UInt32 size = reply->m_data.size();
			if (!CXWindowsUtil::setWindowProperty(m_display,
								reply->m_requestor, reply->m_property,
								&size, 4, m_atomINCR, 32)) {
				failed = true;
			}
		}

		// send more INCR reply or entire non-incremental reply
		else {
			// how much more data should we send?
			UInt32 size = reply->m_data.size() - reply->m_ptr;
			if (size > maxRequestSize)
				size = maxRequestSize;

			// send it
			if (!CXWindowsUtil::setWindowProperty(m_display,
								reply->m_requestor, reply->m_property,
								reply->m_data.data() + reply->m_ptr,
								size,
								reply->m_type, reply->m_format)) {
				failed = true;
			}
			else {
				reply->m_ptr += size;

				// we've finished the reply if we just sent the zero
				// size incremental chunk or if we're not incremental.
				reply->m_done = (size == 0 || !useINCR);
			}
		}
	}

	// if we've failed then delete the property and say we're done.
	// if we haven't replied yet then we can send a failure notify,
	// otherwise we've failed in the middle of an incremental
	// transfer;  i don't know how to cancel that so i'll just send
	// the final zero-length property.
	// FIXME -- how do you gracefully cancel an incremental transfer?
	if (failed) {
		LOG((CLOG_DEBUG1 "clipboard: sending failure to 0x%08x,%d,%d", reply->m_requestor, reply->m_target, reply->m_property));
		reply->m_done = true;
		if (reply->m_property != None) {
			CXWindowsUtil::CErrorLock lock(m_display);
			XDeleteProperty(m_display, reply->m_requestor, reply->m_property);
		}

		if (!reply->m_replied) {
			sendNotify(reply->m_requestor, m_selection,
								reply->m_target, None,
								reply->m_time);

			// don't wait for any reply (because we're not expecting one)
			return true;
		}
		else {
			static const char dummy = 0;
			CXWindowsUtil::setWindowProperty(m_display,
								reply->m_requestor, reply->m_property,
								&dummy,
								0,
								reply->m_type, reply->m_format);

			// wait for delete notify
			return false;
		}
	}

	// send notification if we haven't yet
	if (!reply->m_replied) {
		LOG((CLOG_DEBUG1 "clipboard: sending notify to 0x%08x,%d,%d", reply->m_requestor, reply->m_target, reply->m_property));
		reply->m_replied = true;

		// dump every property on the requestor window to the debug2
		// log.  we've seen what appears to be a bug in lesstif and
		// knowing the properties may help design a workaround, if
		// it becomes necessary.
		if (CLOG->getFilter() >= CLog::kDEBUG2) {
			CXWindowsUtil::CErrorLock lock(m_display);
			int n;
			Atom* props = XListProperties(m_display, reply->m_requestor, &n);
			LOG((CLOG_DEBUG2 "properties of 0x%08x:", reply->m_requestor));
			for (int i = 0; i < n; ++i) {
				Atom target;
				CString data;
				char* name = XGetAtomName(m_display, props[i]);
				if (!CXWindowsUtil::getWindowProperty(m_display,
								reply->m_requestor,
								props[i], &data, &target, NULL, False)) {
					LOG((CLOG_DEBUG2 "  %s: <can't read property>", name));
				}
				else {
					// if there are any non-ascii characters in string
					// then print the binary data.
					static const char* hex = "0123456789abcdef";
					for (CString::size_type j = 0; j < data.size(); ++j) {
						if (data[j] < 32 || data[j] > 126) {
							CString tmp;
							tmp.reserve(data.size() * 3);
							for (j = 0; j < data.size(); ++j) {
								unsigned char v = (unsigned char)data[j];
								tmp += hex[v >> 16];
								tmp += hex[v & 15];
								tmp += ' ';
							}
							data = tmp;
							break;
						}
					}
					char* type = XGetAtomName(m_display, target);
					LOG((CLOG_DEBUG2 "  %s (%s): %s", name, type, data.c_str()));
					if (type != NULL) {
						XFree(type);
					}
				}
				if (name != NULL) {
					XFree(name);
				}
			}
			if (props != NULL) {
				XFree(props);
			}
		}

		sendNotify(reply->m_requestor, m_selection,
								reply->m_target, reply->m_property,
								reply->m_time);
	}

	// wait for delete notify
	return false;
}

void
CXWindowsClipboard::clearReplies()
{
	for (CReplyMap::iterator index = m_replies.begin();
								index != m_replies.end(); ++index) {
		clearReplies(index->second);
	}
	m_replies.clear();
	m_eventMasks.clear();
}

void
CXWindowsClipboard::clearReplies(CReplyList& replies)
{
	for (CReplyList::iterator index = replies.begin();
								index != replies.end(); ++index) {
		delete *index;
	}
	replies.clear();
}

void
CXWindowsClipboard::sendNotify(Window requestor,
				Atom selection, Atom target, Atom property, Time time)
{
	XEvent event;
	event.xselection.type      = SelectionNotify;
	event.xselection.display   = m_display;
	event.xselection.requestor = requestor;
	event.xselection.selection = selection;
	event.xselection.target    = target;
	event.xselection.property  = property;
	event.xselection.time      = time;
	CXWindowsUtil::CErrorLock lock(m_display);
	XSendEvent(m_display, requestor, False, 0, &event);
}

bool
CXWindowsClipboard::wasOwnedAtTime(::Time time) const
{
	// not owned if we've never owned the selection
	checkCache();
	if (m_timeOwned == 0) {
		return false;
	}

	// if time is CurrentTime then return true if we still own the
	// selection and false if we do not.  else if we still own the
	// selection then get the current time, otherwise use
	// m_timeLost as the end time.
	Time lost = m_timeLost;
	if (m_timeLost == 0) {
		if (time == CurrentTime) {
			return true;
		}
		else {
			lost = CXWindowsUtil::getCurrentTime(m_display, m_window);
		}
	}
	else {
		if (time == CurrentTime) {
			return false;
		}
	}

	// compare time to range
	Time duration = lost - m_timeOwned;
	Time when     = time - m_timeOwned;
	return (/*when >= 0 &&*/ when < duration);
}

Atom
CXWindowsClipboard::getTargetsData(CString& data, int* format) const
{
	assert(format != NULL);

	// add standard targets
	Atom atom;
	atom = m_atomTargets;
	data.append(reinterpret_cast<char*>(&atom), sizeof(Atom));
	atom = m_atomMultiple;
	data.append(reinterpret_cast<char*>(&atom), sizeof(Atom));
	atom = m_atomTimestamp;
	data.append(reinterpret_cast<char*>(&atom), sizeof(Atom));

	// add targets we can convert to
	for (ConverterList::const_iterator index = m_converters.begin();
								index != m_converters.end(); ++index) {
		IXWindowsClipboardConverter* converter = *index;

		// skip formats we don't have
		if (m_added[converter->getFormat()]) {
			atom = converter->getAtom();
			data.append(reinterpret_cast<char*>(&atom), sizeof(Atom));
		}
	}

	*format = 32;
	return m_atomAtom;
}

Atom
CXWindowsClipboard::getTimestampData(CString& data, int* format) const
{
	assert(format != NULL);

	assert(sizeof(m_timeOwned) == 4);
	checkCache();
	data.append(reinterpret_cast<const char*>(&m_timeOwned), 4);
	*format = 32;
	return m_atomInteger;
}


//
// CXWindowsClipboard::CICCCMGetClipboard
//

CXWindowsClipboard::CICCCMGetClipboard::CICCCMGetClipboard(
				Window requestor, Time time, Atom property) :
	m_requestor(requestor),
	m_time(time),
	m_property(property),
	m_incr(false),
	m_failed(false),
	m_done(false),
	m_reading(false),
	m_data(NULL),
	m_actualTarget(NULL),
	m_error(false)
{
	// do nothing
}

CXWindowsClipboard::CICCCMGetClipboard::~CICCCMGetClipboard()
{
	// do nothing
}

bool
CXWindowsClipboard::CICCCMGetClipboard::readClipboard(Display* display,
				Atom selection, Atom target, Atom* actualTarget, CString* data)
{
	assert(actualTarget != NULL);
	assert(data         != NULL);

	LOG((CLOG_DEBUG1 "request selection=%s, target=%s, window=%x", CXWindowsUtil::atomToString(display, selection).c_str(), CXWindowsUtil::atomToString(display, target).c_str(), m_requestor));

	// save output pointers
	m_actualTarget = actualTarget;
	m_data         = data;

	// assume failure
	*m_actualTarget = None;
	*m_data         = "";

	// delete target property
	XDeleteProperty(display, m_requestor, m_property);

	// select window for property changes
	XWindowAttributes attr;
	XGetWindowAttributes(display, m_requestor, &attr);
	XSelectInput(display, m_requestor,
								attr.your_event_mask | PropertyChangeMask);

	// request data conversion
	XConvertSelection(display, selection, target,
								m_property, m_requestor, m_time);

	// synchronize with server before we start following timeout countdown
	XSync(display, False);

	// Xlib inexplicably omits the ability to wait for an event with
	// a timeout.  (it's inexplicable because there's no portable way
	// to do it.)  we'll poll until we have what we're looking for or
	// a timeout expires.  we use a timeout so we don't get locked up
	// by badly behaved selection owners.
	XEvent xevent;
	std::vector<XEvent> events;
	CStopwatch timeout(true);
	static const double s_timeout = 0.25;	// FIXME -- is this too short?
	bool noWait = false;
	while (!m_done && !m_failed) {
		// fail if timeout has expired
		if (timeout.getTime() >= s_timeout) {
			m_failed = true;
			break;
		}

		// process events if any otherwise sleep
		if (noWait || XPending(display) > 0) {
			while (!m_done && !m_failed && (noWait || XPending(display) > 0)) {
				XNextEvent(display, &xevent);
				if (!processEvent(display, &xevent)) {
					// not processed so save it
					events.push_back(xevent);
				}
				else {
					// reset timer since we've made some progress
					timeout.reset();

					// don't sleep anymore, just block waiting for events.
					// we're assuming here that the clipboard owner will
					// complete the protocol correctly.  if we continue to
					// sleep we'll get very bad performance.
					noWait = true;
				}
			}
		}
		else {
			ARCH->sleep(0.01);
		}
	}

	// put unprocessed events back
	for (UInt32 i = events.size(); i > 0; --i) {
		XPutBackEvent(display, &events[i - 1]);
	}

	// restore mask
	XSelectInput(display, m_requestor, attr.your_event_mask);

	// return success or failure
	LOG((CLOG_DEBUG1 "request %s", m_failed ? "failed" : "succeeded"));
	return !m_failed;
}

bool
CXWindowsClipboard::CICCCMGetClipboard::processEvent(
				Display* display, XEvent* xevent)
{
	// process event
	switch (xevent->type) {
	case DestroyNotify:
		if (xevent->xdestroywindow.window == m_requestor) {
			m_failed = true;
			return true;
		}

		// not interested
		return false;

	case SelectionNotify:
		if (xevent->xselection.requestor == m_requestor) {
			// done if we can't convert
			if (xevent->xselection.property == None) {
				m_done = true;
				return true;
			}

			// proceed if conversion successful
			else if (xevent->xselection.property == m_property) {
				m_reading = true;
				break;
			}
		}

		// otherwise not interested
		return false;

	case PropertyNotify:
		// proceed if conversion successful and we're receiving more data
		if (xevent->xproperty.window == m_requestor &&
			xevent->xproperty.atom   == m_property &&
			xevent->xproperty.state  == PropertyNewValue) {
			if (!m_reading) {
				// we haven't gotten the SelectionNotify yet
				return true;
			}
			break;
		}

		// otherwise not interested
		return false;

	default:
		// not interested
		return false;
	}

	// get the data from the property
	Atom target;
	const CString::size_type oldSize = m_data->size();
	if (!CXWindowsUtil::getWindowProperty(display, m_requestor,
								m_property, m_data, &target, NULL, True)) {
		// unable to read property
		m_failed = true;
		return true;
	}

	// note if incremental.  if we're already incremental then the
	// selection owner is busted.  if the INCR property has no size
	// then the selection owner is busted.
	if (target == XInternAtom(display, "INCR", False)) {
		if (m_incr) {
			m_failed = true;
			m_error  = true;
		}
		else if (m_data->size() == oldSize) {
			m_failed = true;
			m_error  = true;
		}
		else {
			m_incr   = true;

			// discard INCR data
			*m_data = "";
		}
	}

	// handle incremental chunks
	else if (m_incr) {
		// if first incremental chunk then save target
		if (oldSize == 0) {
			LOG((CLOG_DEBUG1 "  INCR first chunk, target %s", CXWindowsUtil::atomToString(display, target).c_str()));
			*m_actualTarget = target;
		}

		// secondary chunks must have the same target
		else {
			if (target != *m_actualTarget) {
				LOG((CLOG_WARN "  INCR target mismatch"));
				m_failed = true;
				m_error  = true;
			}
		}

		// note if this is the final chunk
		if (m_data->size() == oldSize) {
			LOG((CLOG_DEBUG1 "  INCR final chunk: %d bytes total", m_data->size()));
			m_done = true;
		}
	}

	// not incremental;  save the target.
	else {
		LOG((CLOG_DEBUG1 "  target %s", CXWindowsUtil::atomToString(display, target).c_str()));
		*m_actualTarget = target;
		m_done          = true;
	}

	// this event has been processed
	LOGC(!m_incr, (CLOG_DEBUG1 "  got data, %d bytes", m_data->size()));
	return true;
}


//
// CXWindowsClipboard::CReply
//

CXWindowsClipboard::CReply::CReply(Window requestor, Atom target, ::Time time) :
	m_requestor(requestor),
	m_target(target),
	m_time(time),
	m_property(None),
	m_replied(false),
	m_done(false),
	m_data(),
	m_type(None),
	m_format(32),
	m_ptr(0)
{
	// do nothing
}

CXWindowsClipboard::CReply::CReply(Window requestor, Atom target, ::Time time,
				Atom property, const CString& data, Atom type, int format) :
	m_requestor(requestor),
	m_target(target),
	m_time(time),
	m_property(property),
	m_replied(false),
	m_done(false),
	m_data(data),
	m_type(type),
	m_format(format),
	m_ptr(0)
{
	// do nothing
}