/*
 * Deskflow -- mouse and keyboard sharing utility
 * Copyright (C) 2012-2016 Symless Ltd.
 * 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 LICENSE 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "platform/XWindowsClipboard.h"

#include "arch/Arch.h"
#include "base/Log.h"
#include "base/Stopwatch.h"
#include "common/basic_types.h"
#include "common/stdvector.h"
#include "platform/XWindowsClipboardBMPConverter.h"
#include "platform/XWindowsClipboardHTMLConverter.h"
#include "platform/XWindowsClipboardTextConverter.h"
#include "platform/XWindowsClipboardUCS2Converter.h"
#include "platform/XWindowsClipboardUTF8Converter.h"
#include "platform/XWindowsUtil.h"

#include <X11/Xatom.h>
#include <algorithm>
#include <cstdio>
#include <cstring>

//
// XWindowsClipboard
//

XWindowsClipboard::XWindowsClipboard(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;

  default:
    m_selection = XA_PRIMARY;
    break;
  }

  // add converters, most desired first
  m_converters.push_back(new XWindowsClipboardHTMLConverter(m_display, "text/html"));
  m_converters.push_back(new XWindowsClipboardHTMLConverter(m_display, "application/x-moz-nativehtml"));
  m_converters.push_back(new XWindowsClipboardBMPConverter(m_display));
  m_converters.push_back(new XWindowsClipboardUTF8Converter(m_display, "text/plain;charset=UTF-8", true));
  m_converters.push_back(new XWindowsClipboardUTF8Converter(m_display, "text/plain;charset=utf-8", true));
  m_converters.push_back(new XWindowsClipboardUTF8Converter(m_display, "UTF8_STRING"));
  m_converters.push_back(new XWindowsClipboardUCS2Converter(m_display, "text/plain;charset=ISO-10646-UCS-2"));
  m_converters.push_back(new XWindowsClipboardUCS2Converter(m_display, "text/unicode"));
  m_converters.push_back(new XWindowsClipboardTextConverter(m_display, "text/plain"));
  m_converters.push_back(new XWindowsClipboardTextConverter(m_display, "STRING"));

  // we have no data
  clearCache();
}

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

void XWindowsClipboard::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 XWindowsClipboard::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,
         XWindowsUtil::atomToString(m_display, target).c_str(), requestor,
         XWindowsUtil::atomToString(m_display, property).c_str())
    );
    if (wasOwnedAtTime(time)) {
      if (target == m_atomMultiple && property != None) {
        // add a multiple request.  property may not be None
        // according to ICCCM.
        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 Reply(requestor, target, time));
  }

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

bool XWindowsClipboard::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
  String 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 != nullptr) {
      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
          LOG((CLOG_WARN "error while converting clipboard data"));
        }
      }
    }
  }

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

bool XWindowsClipboard::processRequest(Window requestor, ::Time /*time*/, Atom property)
{
  ReplyMap::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", XWindowsUtil::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.
  ReplyList &replies = index->second;
  for (ReplyList::iterator index2 = replies.begin(); index2 != replies.end(); ++index2) {
    Reply *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 XWindowsClipboard::destroyRequest(Window requestor)
{
  ReplyMap::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 XWindowsClipboard::getWindow() const
{
  return m_window;
}

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

bool XWindowsClipboard::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 XWindowsClipboard::add(EFormat format, const String &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 XWindowsClipboard::open(Time time) const
{
  if (m_open) {
    LOG((CLOG_DEBUG "failed to open clipboard: already opened"));
    return false;
  }

  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 XWindowsClipboard::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 XWindowsClipboard::getTime() const
{
  checkCache();
  return m_timeOwned;
}

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

  fillCache();
  return m_added[format];
}

String XWindowsClipboard::get(EFormat format) const
{
  assert(m_open);

  fillCache();
  return m_data[format];
}

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

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

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

  return converter;
}

void XWindowsClipboard::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 XWindowsClipboard::clearCache() const
{
  const_cast<XWindowsClipboard *>(this)->doClearCache();
}

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

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

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

void XWindowsClipboard::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;
  String data;
  if (!icccmGetSelection(atomTargets, &target, &data) || (target != m_atomAtom && target != m_atomTargets)) {
    LOG((CLOG_DEBUG1 "selection doesn't support TARGETS"));
    data = "";
    XWindowsUtil::appendAtomData(data, XA_STRING);
  }

  XWindowsUtil::convertAtomProperty(data);
  auto targets = static_cast<const Atom *>(static_cast<const void *>(data.data()));
  const UInt32 numTargets = data.size() / sizeof(Atom);
  LOG((CLOG_DEBUG "  available targets: %s", XWindowsUtil::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;
    // XXX -- just ask for the converter's target to see if it's
    // available rather than checking TARGETS.  i've seen clipboard
    // owners that don't report all the targets they support.
    target = converter->getAtom();
    /*
    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;
    String targetData;
    if (!icccmGetSelection(target, &actualTarget, &targetData)) {
      LOG((CLOG_DEBUG1 "  no data for target %s", XWindowsUtil::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,
         XWindowsUtil::atomToString(m_display, target).c_str(), targetData.size(),
         targetData.size() == 1 ? "byte" : "bytes")
    );
  }
}

bool XWindowsClipboard::icccmGetSelection(Atom target, Atom *actualTarget, String *data) const
{
  assert(actualTarget != nullptr);
  assert(data != nullptr);

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

IClipboard::Time XWindowsClipboard::icccmGetTime() const
{
  Atom actualTarget;
  String data;
  if (icccmGetSelection(m_atomTimestamp, &actualTarget, &data) && actualTarget == m_atomInteger) {
    Time time = *static_cast<const Time *>(static_cast<const void *>(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 XWindowsClipboard::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 = XWindowsUtil::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 XWindowsClipboard::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 = XWindowsUtil::getCurrentTime(m_display, m_window);
  XSetSelectionOwner(m_display, m_atomMotifClipLock, None, time);
}

bool XWindowsClipboard::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;
  String data;
  Window root = RootWindow(m_display, DefaultScreen(m_display));
  if (!XWindowsUtil::getWindowProperty(m_display, root, m_atomMotifClipHeader, &data, &target, &format, False)) {
    return false;
  }

  // check the owner window against the current clipboard owner
  if (data.size() >= sizeof(MotifClipHeader)) {
    MotifClipHeader header;
    std::memcpy(&header, data.data(), sizeof(header));
    if ((header.m_id == MotifClip::Header) && (static_cast<Window>(header.m_selectionOwner) == owner)) {
      return true;
    }
  }

  return false;
}

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

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

  MotifClipHeader header;
  if (data.size() < sizeof(header)) { // check that the header is okay
    return;
  }
  std::memcpy(&header, data.data(), sizeof(header));
  if (header.m_id != MotifClip::Header || header.m_numItems < 1) {
    return;
  }

  // get the Motif item property from the root window
  static const int buffer_size = 18 + 20;
  char name[buffer_size];
  snprintf(name, buffer_size, "_MOTIF_CLIP_ITEM_%d", header.m_item);
  Atom atomItem = XInternAtom(m_display, name, False);
  data = "";
  if (!XWindowsUtil::getWindowProperty(m_display, root, atomItem, &data, &target, &format, False)) {
    return;
  }

  MotifClipItem item;
  if (data.size() < sizeof(item)) { // check that the item is okay
    return;
  }
  std::memcpy(&item, data.data(), sizeof(item));
  if (item.m_id != MotifClip::Item || 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;
  auto formats = static_cast<const SInt32 *>(static_cast<const void *>(item.m_size + data.data()));

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

    // check that the format is okay
    MotifClipFormat motifFormat;
    if (data.size() < sizeof(motifFormat)) {
      continue;
    }
    std::memcpy(&motifFormat, data.data(), sizeof(motifFormat));
    if (motifFormat.m_id != MotifClip::Format || 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
    MotifFormatMap::const_iterator index2 = motifFormats.find(converter->getAtom());
    if (index2 == motifFormats.end()) {
      continue;
    }

    // get format
    MotifClipFormat motifFormat;
    std::memcpy(&motifFormat, index2->second.data(), sizeof(motifFormat));
    const Atom target = motifFormat.m_type;

    // get the data (finally)
    Atom actualTarget;
    String targetData;
    if (!motifGetSelection(&motifFormat, &actualTarget, &targetData)) {
      LOG((CLOG_DEBUG1 "  no data for target %s", XWindowsUtil::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, XWindowsUtil::atomToString(m_display, target).c_str()));
  }
}

bool XWindowsClipboard::motifGetSelection(const MotifClipFormat *format, Atom *actualTarget, String *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.
  static const int buffer_size = 18 + 20;
  char name[buffer_size];
  snprintf(name, buffer_size, "_MOTIF_CLIP_ITEM_%d", format->m_data);
  Atom target = XInternAtom(m_display, name, False);
  Window root = RootWindow(m_display, DefaultScreen(m_display));
  return XWindowsUtil::getWindowProperty(m_display, root, target, data, actualTarget, nullptr, False);
}

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

bool XWindowsClipboard::insertMultipleReply(Window requestor, ::Time time, Atom property)
{
  // get the requested targets
  Atom target;
  SInt32 format;
  String data;
  if (!XWindowsUtil::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
  XWindowsUtil::convertAtomProperty(data);
  auto targets = static_cast<const Atom *>(static_cast<const void *>(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
      XWindowsUtil::replaceAtomData(data, i, None);
      changed = true;
    }
  }

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

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

  return true;
}

void XWindowsClipboard::insertReply(Reply *reply)
{
  assert(reply != nullptr);

  // 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;
    {
      XWindowsUtil::ErrorLock 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 XWindowsClipboard::pushReplies()
{
  // send the first reply for each window if that reply hasn't
  // been sent yet.
  for (ReplyMap::iterator index = m_replies.begin(); index != m_replies.end();) {
    assert(!index->second.empty());
    ReplyList::iterator listit = index->second.begin();
    while (listit != index->second.end()) {
      if (!(*listit)->m_replied)
        break;
      ++listit;
    }
    if (listit != index->second.end() && !(*listit)->m_replied) {
      pushReplies(index, index->second, listit);
    } else {
      ++index;
    }
  }
}

void XWindowsClipboard::pushReplies(ReplyMap::iterator &mapIndex, ReplyList &replies, ReplyList::iterator index)
{
  Reply *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()) {
    XWindowsUtil::ErrorLock lock(m_display);
    Window requestor = mapIndex->first;
    XSelectInput(m_display, requestor, m_eventMasks[requestor]);
    m_replies.erase(mapIndex++);
    m_eventMasks.erase(requestor);
  } else {
    ++mapIndex;
  }
}

bool XWindowsClipboard::sendReply(Reply *reply)
{
  assert(reply != nullptr);

  // 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 (!XWindowsUtil::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 (!XWindowsUtil::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) {
      XWindowsUtil::ErrorLock 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;
      XWindowsUtil::setWindowProperty(
          m_display, reply->m_requestor, reply->m_property, &dummy, 0, reply->m_type, reply->m_format
      );

      // wait for delete notify
      return false;
    }
  }

  // notification already sended
  if (reply->m_replied) {
    return false;
  }

  LOG((CLOG_DEBUG1 "clipboard: sending notify to 0x%08x,%d,%d", reply->m_requestor, reply->m_target, reply->m_property)
  );
  reply->m_replied = true;

  // nothing to log
  if (CLOG->getFilter() < kDEBUG2) {
    sendNotify(
        reply->m_requestor, m_selection, reply->m_target, reply->m_property, static_cast<unsigned int>(reply->m_time)
    );

    // wait for delete notify
    return false;
  }

  // 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.
  XWindowsUtil::ErrorLock 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;
    String data;
    char *name = XGetAtomName(m_display, props[i]);
    if (!XWindowsUtil::getWindowProperty(m_display, reply->m_requestor, props[i], &data, &target, nullptr, False)) {
      LOG((CLOG_DEBUG2 "  %s: <can't read property>", name));
    } else {
      // convert to hex if contains non ascii symbols
      if (std::find_if(data.begin(), data.end(), [](const unsigned char &c) { return c < 32 || c > 126; }) !=
          data.end()) {
        const String hex_digits = "0123456789abcdef";
        String tmp;
        tmp.reserve(data.length() * 3);
        std::for_each(data.begin(), data.end(), [hex_digits, &tmp](const unsigned char &c) {
          tmp += hex_digits[c >> 16];
          tmp += hex_digits[c & 15];
          tmp += ' ';
        });
        data = tmp;
      }
      char *type = XGetAtomName(m_display, target);
      LOG((CLOG_DEBUG2 "  %s (%s): %s", name, type, data.c_str()));
      if (type != nullptr) {
        XFree(type);
      }
    }
    if (name != nullptr) {
      XFree(name);
    }
  }
  if (props != nullptr) {
    XFree(props);
  }

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

  // wait for delete notify
  return false;
}

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

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

void XWindowsClipboard::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;
  XWindowsUtil::ErrorLock lock(m_display);
  XSendEvent(m_display, requestor, False, 0, &event);
}

bool XWindowsClipboard::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 = XWindowsUtil::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 XWindowsClipboard::getTargetsData(String &data, int *format) const
{
  assert(format != nullptr);

  // add standard targets
  XWindowsUtil::appendAtomData(data, m_atomTargets);
  XWindowsUtil::appendAtomData(data, m_atomMultiple);
  XWindowsUtil::appendAtomData(data, m_atomTimestamp);

  // 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()]) {
      XWindowsUtil::appendAtomData(data, converter->getAtom());
    }
  }

  *format = 32;
  return m_atomAtom;
}

Atom XWindowsClipboard::getTimestampData(String &data, int *format) const
{
  assert(format != nullptr);

  checkCache();
  XWindowsUtil::appendTimeData(data, m_timeOwned);
  *format = 32;
  return m_atomInteger;
}

//
// XWindowsClipboard::CICCCMGetClipboard
//

XWindowsClipboard::CICCCMGetClipboard::CICCCMGetClipboard(Window requestor, Time time, Atom property)
    : m_requestor(requestor),
      m_time(time),
      m_property(property)
{
}

bool XWindowsClipboard::CICCCMGetClipboard::readClipboard(
    Display *display, Atom selection, Atom target, Atom *actualTarget, String *data
)
{
  assert(actualTarget != nullptr);
  assert(data != nullptr);

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

  m_atomNone = XInternAtom(display, "NONE", False);
  m_atomIncr = XInternAtom(display, "INCR", False);

  // 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;
  Stopwatch timeout(false);             // timer not stopped, not triggered
  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)) {
        // fail if timeout has expired
        if (timeout.getTime() >= s_timeout) {
          m_failed = true;
          break;
        }

        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 after %fs", m_failed ? "failed" : "succeeded", timeout.getTime()));
  return !m_failed;
}

bool XWindowsClipboard::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 || xevent->xselection.property == m_atomNone) {
        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 String::size_type oldSize = m_data->size();
  if (!XWindowsUtil::getWindowProperty(display, m_requestor, m_property, m_data, &target, nullptr, 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 == m_atomIncr) {
    if (m_incr || 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", XWindowsUtil::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", XWindowsUtil::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;
}

//
// XWindowsClipboard::Reply
//

XWindowsClipboard::Reply::Reply(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
}

XWindowsClipboard::Reply::Reply(
    Window requestor, Atom target, ::Time time, Atom property, const String &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
}
