/**
 * Looking Glass
 * Copyright © 2017-2026 The Looking Glass Authors
 * https://looking-glass.io
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program 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, write to the Free Software Foundation, Inc., 59
 * Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

#include "core/clipboard.h"
#include "core/clipboard_files.h"
#include "main.h"

#include "common/debug.h"
#include "common/ll.h"
#include "common/locking.h"

#include <limits.h>
#include <stdlib.h>
#include <string.h>

#define COMPAT_CHUNK_SIZE (64U * 1024U)

typedef struct ClipboardBinding
{
  const LG_ClipboardOps * ops;
  void                  * opaque;
  bool                    available;
  uint32_t                generation;
}
ClipboardBinding;

typedef struct ClipboardRequest
{
  LG_ClipboardRequest request;
  ClipboardBinding    binding;
  uint32_t            remoteGeneration;
  LG_ClipboardData    type;
  LG_ClipboardReplyFn replyFn;
  const LG_ClipboardStreamOps * stream;
  void                        * opaque;

  bool     started;
  bool     blocked;
  uint64_t sizeHint;
  uint64_t offset;

  uint8_t * buffer;
  size_t    bufferSize;
  size_t    bufferCapacity;

  bool             compat;
  unsigned int     compatPhase;
  LG_ClipboardData compatType;
  uint8_t        * compatData;
  size_t           compatSize;
}
ClipboardRequest;

enum
{
  COMPAT_BEGIN,
  COMPAT_CHUNK,
  COMPAT_END,
};

static struct
{
  LG_Lock   registrationLock;
  LG_Lock   providerLock;
  LG_RWLock activeLock;
  LG_Lock   stateLock;
  LG_Lock   requestLock;
  LG_Lock   writeLock;
  LG_Lock   callbackLock;
  LG_Lock   fileLock;

  ClipboardBinding fallback;
  ClipboardBinding transport;
  ClipboardBinding active;

  bool             localAvailable;
  LG_ClipboardData localTypes[LG_CLIPBOARD_DATA_NONE];
  size_t           localTypeCount;
  uint64_t         localFileDataset;

  bool             remoteNotice;
  LG_ClipboardData remoteType;
  uint32_t         remoteGeneration;
  uint64_t         remoteFileDataset;

  bool                remoteRequest;
  ClipboardBinding    remoteRequestBinding;
  LG_ClipboardRequest remoteRequestId;
  LG_ClipboardRequest remoteTransferId;
  LG_ClipboardData    remoteRequestType;
  bool                remoteStarted;
  bool                remoteBlocked;
  uint64_t            remoteSizeHint;
  uint64_t            remoteOffset;

  uint8_t * remoteBuffer;
  size_t    remoteBufferSize;
  size_t    remoteBufferCapacity;

  bool         remoteCompat;
  unsigned int remoteCompatPhase;

  LG_ClipboardRequest requestSerial;
  LG_ClipboardRequest transferSerial;
  struct ll         * requests;
}
clipboard;

static LG_ClipboardResult pumpRemoteCompat(void);

static bool validType(LG_ClipboardData type)
{
  return type >= LG_CLIPBOARD_DATA_TEXT &&
    type < LG_CLIPBOARD_DATA_NONE;
}

static uint32_t nextGeneration(uint32_t generation)
{
  if (++generation == 0)
    ++generation;
  return generation;
}

static LG_ClipboardRequest nextRequestNL(void)
{
  if (++clipboard.requestSerial == LG_CLIPBOARD_REQUEST_INVALID)
    ++clipboard.requestSerial;
  return clipboard.requestSerial;
}

static LG_ClipboardRequest nextTransferNL(void)
{
  if (++clipboard.transferSerial == LG_CLIPBOARD_REQUEST_INVALID)
    ++clipboard.transferSerial;
  return clipboard.transferSerial;
}

static bool bindingEqual(const ClipboardBinding * a,
    const ClipboardBinding * b)
{
  return a->ops        == b->ops &&
    a->opaque          == b->opaque &&
    a->generation      == b->generation;
}

static bool bindingActiveNL(const ClipboardBinding * binding)
{
  return binding->ops && bindingEqual(binding, &clipboard.active);
}

static bool localHasTypeNL(LG_ClipboardData type)
{
  for (size_t i = 0; i < clipboard.localTypeCount; ++i)
    if (clipboard.localTypes[i] == type)
      return true;

  return false;
}

static bool validStream(const LG_ClipboardStreamOps * stream)
{
  return stream && stream->begin && stream->chunk &&
    stream->end && stream->cancel;
}

static bool streamProvider(const LG_ClipboardOps * ops)
{
  return ops && ops->dataBegin && ops->dataChunk && ops->dataEnd &&
    ops->dataCancel && ops->dataReady;
}

static bool growBuffer(uint8_t ** buffer, size_t * capacity, size_t size)
{
  if (size <= *capacity)
    return true;

  size_t next = *capacity ? *capacity : 4096;
  while (next < size)
  {
    if (next > SIZE_MAX / 2)
    {
      next = size;
      break;
    }
    next *= 2;
  }

  uint8_t * resized = realloc(*buffer, next);
  if (!resized)
    return false;

  *buffer   = resized;
  *capacity = next;
  return true;
}

static LG_ClipboardResult legacyBegin(void * opaque,
    LG_ClipboardData type, uint64_t sizeHint)
{
  ClipboardRequest * request = opaque;
  if (type != request->type ||
      (sizeHint != LG_CLIPBOARD_SIZE_UNKNOWN && sizeHint > UINT32_MAX))
    return LG_CLIPBOARD_RESULT_FAILED;

  free(request->buffer);
  request->buffer         = NULL;
  request->bufferSize     = 0;
  request->bufferCapacity = 0;
  return LG_CLIPBOARD_RESULT_ACCEPTED;
}

static LG_ClipboardResult legacyChunk(void * opaque, uint64_t offset,
    const void * data, size_t size)
{
  ClipboardRequest * request = opaque;
  if (offset != request->bufferSize || !size || !data ||
      offset > UINT32_MAX || size > UINT32_MAX - offset)
    return LG_CLIPBOARD_RESULT_FAILED;

  const size_t required = request->bufferSize + size;
  if (!growBuffer(&request->buffer, &request->bufferCapacity, required))
    return LG_CLIPBOARD_RESULT_FAILED;

  memcpy(request->buffer + request->bufferSize, data, size);
  request->bufferSize = required;
  return LG_CLIPBOARD_RESULT_ACCEPTED;
}

static LG_ClipboardResult legacyEnd(void * opaque, uint64_t finalSize)
{
  ClipboardRequest * request = opaque;
  if (finalSize != request->bufferSize || finalSize > UINT32_MAX)
    return LG_CLIPBOARD_RESULT_FAILED;

  request->replyFn(request->opaque, request->type,
      request->bufferSize ? request->buffer : NULL,
      (uint32_t)request->bufferSize);
  return LG_CLIPBOARD_RESULT_ACCEPTED;
}

static void legacyCancel(void * opaque, LG_ClipboardCancelReason reason)
{
  (void)reason;
  ClipboardRequest * request = opaque;
  request->replyFn(
      request->opaque, LG_CLIPBOARD_DATA_NONE, NULL, 0);
}

static const LG_ClipboardStreamOps legacyStream =
{
  .begin  = legacyBegin,
  .chunk  = legacyChunk,
  .end    = legacyEnd,
  .cancel = legacyCancel,
};

static LG_ClipboardRequest clearRemoteRequestNL(void)
{
  const LG_ClipboardRequest transfer = clipboard.remoteRequest ?
    clipboard.remoteTransferId : LG_CLIPBOARD_REQUEST_INVALID;
  free(clipboard.remoteBuffer);
  clipboard.remoteRequest        = false;
  clipboard.remoteRequestBinding = (ClipboardBinding) { 0 };
  clipboard.remoteRequestId      = LG_CLIPBOARD_REQUEST_INVALID;
  clipboard.remoteTransferId     = LG_CLIPBOARD_REQUEST_INVALID;
  clipboard.remoteRequestType    = LG_CLIPBOARD_DATA_NONE;
  clipboard.remoteStarted        = false;
  clipboard.remoteBlocked        = false;
  clipboard.remoteSizeHint       = LG_CLIPBOARD_SIZE_UNKNOWN;
  clipboard.remoteOffset         = 0;
  clipboard.remoteBuffer         = NULL;
  clipboard.remoteBufferSize     = 0;
  clipboard.remoteBufferCapacity = 0;
  clipboard.remoteCompat         = false;
  clipboard.remoteCompatPhase    = COMPAT_BEGIN;
  return transfer;
}

static void freeRequest(ClipboardRequest * request)
{
  if (!request)
    return;

  free(request->compatData);
  free(request->buffer);
  free(request);
}

static void * requestStreamOpaque(ClipboardRequest * request)
{
  return request->replyFn ? request : request->opaque;
}

static void cancelRequest(ClipboardRequest * request,
    LG_ClipboardCancelReason reason)
{
  request->stream->cancel(requestStreamOpaque(request), reason);
}

static ClipboardRequest * takeCancelableRequest(
    const ClipboardBinding * binding, uint32_t generation, bool all)
{
  if (!clipboard.requests)
    return NULL;

  ClipboardRequest * request;
  ll_lock(clipboard.requests);
  ll_forEachNL(clipboard.requests, item, request)
  {
    if (!all && bindingEqual(&request->binding, binding) &&
        request->remoteGeneration == generation)
      continue;

    ll_removeNL(clipboard.requests, item);
    free(item);
    ll_unlock(clipboard.requests);
    return request;
  }
  ll_unlock(clipboard.requests);
  return NULL;
}

static ClipboardRequest * takeRequest(LG_ClipboardRequest id)
{
  if (!clipboard.requests)
    return NULL;

  ClipboardRequest * request;
  ll_lock(clipboard.requests);
  ll_forEachNL(clipboard.requests, item, request)
  {
    if (request->request != id)
      continue;

    ll_removeNL(clipboard.requests, item);
    free(item);
    ll_unlock(clipboard.requests);
    return request;
  }
  ll_unlock(clipboard.requests);
  return NULL;
}

/* callbackLock must be held while completing canceled requests. */
static void cancelRequestsNL(const ClipboardBinding * binding,
    uint32_t generation, bool all, LG_ClipboardCancelReason reason)
{
  ClipboardRequest * request;
  while ((request = takeCancelableRequest(binding, generation, all)))
  {
    cancelRequest(request, reason);
    freeRequest(request);
  }
}

static void resetRemote(void)
{
  bool release;
  LG_ClipboardRequest transfer;

  LG_LOCK(clipboard.fileLock);
  LG_LOCK(clipboard.requestLock);
  LG_LOCK(clipboard.stateLock);
  release = clipboard.remoteNotice && clipboard.localAvailable &&
    g_params.clipboardToLocal;
  clipboard.remoteNotice      = false;
  clipboard.remoteType        = LG_CLIPBOARD_DATA_NONE;
  clipboard.remoteFileDataset = 0;
  clipboard.remoteGeneration  = nextGeneration(
      clipboard.remoteGeneration);
  transfer = clearRemoteRequestNL();
  LG_UNLOCK(clipboard.stateLock);
  LG_UNLOCK(clipboard.requestLock);

  clipboardFiles_providerUnavailable();

  LG_LOCK(clipboard.callbackLock);
  cancelRequestsNL(NULL, 0, true, LG_CLIPBOARD_CANCEL_UNAVAILABLE);
  if (transfer != LG_CLIPBOARD_REQUEST_INVALID &&
      g_state.ds->cbRequestCancel)
    g_state.ds->cbRequestCancel(
        transfer, LG_CLIPBOARD_CANCEL_UNAVAILABLE);
  LG_LOCK(clipboard.stateLock);
  release = release && clipboard.localAvailable &&
    !clipboard.remoteNotice;
  LG_UNLOCK(clipboard.stateLock);
  if (release)
    g_state.ds->cbRelease();
  LG_UNLOCK(clipboard.callbackLock);
  LG_UNLOCK(clipboard.fileLock);
}

static bool validOps(const LG_ClipboardOps * ops)
{
  return ops && ops->name && ops->attach && ops->detach &&
    ops->release && ops->notifyTypes && ops->request &&
    (ops->data || streamProvider(ops));
}

static ClipboardBinding makeBinding(
    const LG_ClipboardOps * ops, void * opaque)
{
  return (ClipboardBinding)
  {
    .ops        = ops,
    .opaque     = opaque,
    .available  = ops && !ops->setStatusListener,
    .generation = 0,
  };
}

static ClipboardBinding * nextBindingSlotNL(void)
{
  if (clipboard.transport.available)
    return &clipboard.transport;
  if (clipboard.fallback.available)
    return &clipboard.fallback;
  return NULL;
}

static void publishLocal(const ClipboardBinding * binding)
{
  LG_LOCK(clipboard.writeLock);
  LG_ClipboardData types[LG_CLIPBOARD_DATA_NONE];
  size_t count;

  LG_LOCK(clipboard.stateLock);
  count = g_params.clipboardToVM ? clipboard.localTypeCount : 0;
  memcpy(types, clipboard.localTypes, count * sizeof(*types));
  const uint64_t fileDataset = clipboard.localFileDataset;
  LG_UNLOCK(clipboard.stateLock);

  bool result;
  if (count == 1 && types[0] == LG_CLIPBOARD_DATA_FILES)
    result = fileDataset && binding->ops->offerFiles ?
      binding->ops->offerFiles(binding->opaque, fileDataset) :
      binding->ops->release(binding->opaque);
  else
    result = count ? binding->ops->notifyTypes(
        binding->opaque, types, count) :
      binding->ops->release(binding->opaque);
  if (!result)
    DEBUG_WARN("Failed to publish the local clipboard to %s",
        binding->ops->name);
  LG_UNLOCK(clipboard.writeLock);
}

static void eventNotice(void * opaque,
    const LG_ClipboardData types[], size_t count)
{
  ClipboardBinding * binding = opaque;
  if (!types || count == 0)
    return;

  LG_ClipboardData type = LG_CLIPBOARD_DATA_NONE;
  bool hasFiles = false;
  for (size_t i = 0; i < count; ++i)
    if (validType(types[i]))
    {
      if (types[i] == LG_CLIPBOARD_DATA_FILES)
      {
        type = LG_CLIPBOARD_DATA_FILES;
        hasFiles = true;
      }
      else if (type == LG_CLIPBOARD_DATA_NONE)
        type = types[i];
    }
  if (type == LG_CLIPBOARD_DATA_NONE)
    return;

  LG_LOCK(clipboard.fileLock);
  LG_LOCK_SHARED(clipboard.activeLock);
  if (!bindingActiveNL(binding))
  {
    LG_UNLOCK_SHARED(clipboard.activeLock);
    LG_UNLOCK(clipboard.fileLock);
    return;
  }

  const ClipboardBinding current = *binding;
  LG_UNLOCK_SHARED(clipboard.activeLock);
  bool notice;
  bool release;
  uint32_t generation;
  uint64_t fileDataset;
  LG_ClipboardRequest transfer;

  LG_LOCK(clipboard.requestLock);
  LG_LOCK(clipboard.stateLock);
  const bool previousNotice = clipboard.remoteNotice;
  const bool usable = !hasFiles || clipboard.remoteFileDataset;
  clipboard.remoteNotice     = usable;
  clipboard.remoteType       = usable ? type : LG_CLIPBOARD_DATA_NONE;
  clipboard.remoteGeneration = nextGeneration(
      clipboard.remoteGeneration);
  generation = clipboard.remoteGeneration;
  if (!hasFiles || !usable)
    clipboard.remoteFileDataset = 0;
  fileDataset = clipboard.remoteFileDataset;
  transfer = clearRemoteRequestNL();
  notice = usable && clipboard.localAvailable &&
    g_params.clipboardToLocal;
  release = !usable && previousNotice && clipboard.localAvailable &&
    g_params.clipboardToLocal;
  LG_UNLOCK(clipboard.stateLock);
  LG_UNLOCK(clipboard.requestLock);
  if (!hasFiles)
    clipboardFiles_remoteClear();
  LG_UNLOCK(clipboard.fileLock);

  LG_LOCK(clipboard.callbackLock);
  cancelRequestsNL(&current, generation, false,
      LG_CLIPBOARD_CANCEL_REPLACED);
  if (transfer != LG_CLIPBOARD_REQUEST_INVALID &&
      g_state.ds->cbRequestCancel)
    g_state.ds->cbRequestCancel(transfer, LG_CLIPBOARD_CANCEL_REPLACED);
  LG_LOCK(clipboard.stateLock);
  notice = notice && clipboard.localAvailable &&
    clipboard.remoteNotice &&
    clipboard.remoteGeneration == generation;
  release = release && clipboard.localAvailable &&
    !clipboard.remoteNotice &&
    clipboard.remoteGeneration == generation;
  LG_UNLOCK(clipboard.stateLock);
  LG_LOCK_SHARED(clipboard.activeLock);
  const bool active = bindingActiveNL(&current);
  LG_UNLOCK_SHARED(clipboard.activeLock);
  if (notice && active)
  {
    if (type != LG_CLIPBOARD_DATA_FILES ||
        clipboardFiles_remoteReady(fileDataset))
      g_state.ds->cbNotice(type);
  }
  else if (release && active && g_state.ds->cbRelease)
    g_state.ds->cbRelease();
  LG_UNLOCK(clipboard.callbackLock);
}

static bool returnRequest(ClipboardRequest * request)
{
  if (ll_push(clipboard.requests, request))
    return true;

  cancelRequest(request, LG_CLIPBOARD_CANCEL_UNAVAILABLE);
  freeRequest(request);
  return false;
}

/* callbackLock must be held. The returned request has been removed from the
 * request list and must either be returned to it or freed. */
static ClipboardRequest * takeIncoming(ClipboardBinding * binding,
    LG_ClipboardRequest id, ClipboardBinding * current)
{
  if (id == LG_CLIPBOARD_REQUEST_INVALID)
    return NULL;

  LG_LOCK_SHARED(clipboard.activeLock);
  if (!bindingActiveNL(binding))
  {
    LG_UNLOCK_SHARED(clipboard.activeLock);
    return NULL;
  }
  *current = *binding;
  LG_UNLOCK_SHARED(clipboard.activeLock);

  ClipboardRequest * request = takeRequest(id);
  if (!request)
    return NULL;

  LG_LOCK(clipboard.stateLock);
  const bool deliver = clipboard.localAvailable &&
    g_params.clipboardToLocal && clipboard.remoteNotice &&
    request->remoteGeneration == clipboard.remoteGeneration;
  LG_UNLOCK(clipboard.stateLock);

  LG_LOCK_SHARED(clipboard.activeLock);
  const bool active = bindingActiveNL(current);
  LG_UNLOCK_SHARED(clipboard.activeLock);
  if (deliver && active && bindingEqual(&request->binding, current))
    return request;

  cancelRequest(request, LG_CLIPBOARD_CANCEL_UNAVAILABLE);
  freeRequest(request);
  return NULL;
}

static LG_ClipboardResult incomingBegin(ClipboardRequest * request,
    LG_ClipboardData type, uint64_t sizeHint, bool * terminal)
{
  if (request->started || request->blocked || type != request->type ||
      !validType(type))
    goto invalid;

  const LG_ClipboardResult result = request->stream->begin(
      requestStreamOpaque(request), type, sizeHint);
  switch (result)
  {
    case LG_CLIPBOARD_RESULT_ACCEPTED:
      request->started  = true;
      request->sizeHint = sizeHint;
      request->offset   = 0;
      return result;

    case LG_CLIPBOARD_RESULT_BLOCKED:
      request->blocked = true;
      return result;

    case LG_CLIPBOARD_RESULT_FAILED:
      break;
  }

invalid:
  cancelRequest(request, LG_CLIPBOARD_CANCEL_INVALID);
  *terminal = true;
  return LG_CLIPBOARD_RESULT_FAILED;
}

static LG_ClipboardResult incomingChunk(ClipboardRequest * request,
    uint64_t offset, const void * data, size_t size, bool * terminal)
{
  if (!request->started || request->blocked || offset != request->offset ||
      !data || !size || size > UINT64_MAX - offset)
    goto invalid;

  const LG_ClipboardResult result = request->stream->chunk(
      requestStreamOpaque(request), offset, data, size);
  switch (result)
  {
    case LG_CLIPBOARD_RESULT_ACCEPTED:
      request->offset += size;
      return result;

    case LG_CLIPBOARD_RESULT_BLOCKED:
      request->blocked = true;
      return result;

    case LG_CLIPBOARD_RESULT_FAILED:
      break;
  }

invalid:
  cancelRequest(request, LG_CLIPBOARD_CANCEL_INVALID);
  *terminal = true;
  return LG_CLIPBOARD_RESULT_FAILED;
}

static LG_ClipboardResult incomingEnd(ClipboardRequest * request,
    uint64_t finalSize, bool * terminal)
{
  if (!request->started || request->blocked || finalSize != request->offset)
    goto invalid;

  const LG_ClipboardResult result = request->stream->end(
      requestStreamOpaque(request), finalSize);
  switch (result)
  {
    case LG_CLIPBOARD_RESULT_ACCEPTED:
      *terminal = true;
      return result;

    case LG_CLIPBOARD_RESULT_BLOCKED:
      request->blocked = true;
      return result;

    case LG_CLIPBOARD_RESULT_FAILED:
      break;
  }

invalid:
  cancelRequest(request, LG_CLIPBOARD_CANCEL_INVALID);
  *terminal = true;
  return LG_CLIPBOARD_RESULT_FAILED;
}

static LG_ClipboardResult pumpIncomingCompat(ClipboardRequest * request,
    bool * terminal)
{
  for (;;)
  {
    LG_ClipboardResult result;
    switch (request->compatPhase)
    {
      case COMPAT_BEGIN:
        result = incomingBegin(request, request->compatType,
            request->compatSize, terminal);
        if (result != LG_CLIPBOARD_RESULT_ACCEPTED)
          return result;
        request->compatPhase = COMPAT_CHUNK;
        break;

      case COMPAT_CHUNK:
        if (request->offset < request->compatSize)
        {
          const size_t remaining = request->compatSize - request->offset;
          const size_t size = remaining < COMPAT_CHUNK_SIZE ?
            remaining : COMPAT_CHUNK_SIZE;
          result = incomingChunk(request, request->offset,
              request->compatData + request->offset, size, terminal);
          if (result != LG_CLIPBOARD_RESULT_ACCEPTED)
            return result;
          break;
        }
        request->compatPhase = COMPAT_END;
        break;

      case COMPAT_END:
        return incomingEnd(request, request->compatSize, terminal);

      default:
        cancelRequest(request, LG_CLIPBOARD_CANCEL_INVALID);
        *terminal = true;
        return LG_CLIPBOARD_RESULT_FAILED;
    }
  }
}

static LG_ClipboardResult eventDataBegin(void * opaque,
    LG_ClipboardRequest id, LG_ClipboardData type, uint64_t sizeHint)
{
  ClipboardBinding * binding = opaque;
  LG_LOCK(clipboard.callbackLock);
  ClipboardBinding current;
  ClipboardRequest * request = takeIncoming(binding, id, &current);
  if (!request)
  {
    LG_UNLOCK(clipboard.callbackLock);
    return LG_CLIPBOARD_RESULT_FAILED;
  }

  bool terminal = false;
  LG_ClipboardResult result;
  if (request->compat)
  {
    cancelRequest(request, LG_CLIPBOARD_CANCEL_INVALID);
    terminal = true;
    result = LG_CLIPBOARD_RESULT_FAILED;
  }
  else
    result = incomingBegin(request, type, sizeHint, &terminal);
  if (!terminal && !returnRequest(request))
  {
    LG_UNLOCK(clipboard.callbackLock);
    return LG_CLIPBOARD_RESULT_FAILED;
  }
  if (terminal)
    freeRequest(request);
  LG_UNLOCK(clipboard.callbackLock);
  return result;
}

static LG_ClipboardResult eventDataChunk(void * opaque,
    LG_ClipboardRequest id, uint64_t offset, const void * data, size_t size)
{
  ClipboardBinding * binding = opaque;
  LG_LOCK(clipboard.callbackLock);
  ClipboardBinding current;
  ClipboardRequest * request = takeIncoming(binding, id, &current);
  if (!request)
  {
    LG_UNLOCK(clipboard.callbackLock);
    return LG_CLIPBOARD_RESULT_FAILED;
  }

  bool terminal = false;
  LG_ClipboardResult result;
  if (request->compat)
  {
    cancelRequest(request, LG_CLIPBOARD_CANCEL_INVALID);
    terminal = true;
    result = LG_CLIPBOARD_RESULT_FAILED;
  }
  else
    result = incomingChunk(request, offset, data, size, &terminal);
  if (!terminal && !returnRequest(request))
  {
    LG_UNLOCK(clipboard.callbackLock);
    return LG_CLIPBOARD_RESULT_FAILED;
  }
  if (terminal)
    freeRequest(request);
  LG_UNLOCK(clipboard.callbackLock);
  return result;
}

static LG_ClipboardResult eventDataEnd(void * opaque,
    LG_ClipboardRequest id, uint64_t finalSize)
{
  ClipboardBinding * binding = opaque;
  LG_LOCK(clipboard.callbackLock);
  ClipboardBinding current;
  ClipboardRequest * request = takeIncoming(binding, id, &current);
  if (!request)
  {
    LG_UNLOCK(clipboard.callbackLock);
    return LG_CLIPBOARD_RESULT_FAILED;
  }

  bool terminal = false;
  LG_ClipboardResult result;
  if (request->compat)
  {
    cancelRequest(request, LG_CLIPBOARD_CANCEL_INVALID);
    terminal = true;
    result = LG_CLIPBOARD_RESULT_FAILED;
  }
  else
    result = incomingEnd(request, finalSize, &terminal);
  if (!terminal && !returnRequest(request))
  {
    LG_UNLOCK(clipboard.callbackLock);
    return LG_CLIPBOARD_RESULT_FAILED;
  }
  if (terminal)
    freeRequest(request);
  LG_UNLOCK(clipboard.callbackLock);
  return result;
}

static void eventDataCancel(void * opaque, LG_ClipboardRequest id,
    LG_ClipboardCancelReason reason)
{
  ClipboardBinding * binding = opaque;
  LG_LOCK(clipboard.callbackLock);
  ClipboardBinding current;
  ClipboardRequest * request = takeIncoming(binding, id, &current);
  if (request)
  {
    cancelRequest(request, reason);
    freeRequest(request);
  }
  LG_UNLOCK(clipboard.callbackLock);
}

static void eventData(void * opaque, LG_ClipboardRequest id,
    LG_ClipboardData type, const void * data, size_t size)
{
  ClipboardBinding * binding = opaque;
  if (type == LG_CLIPBOARD_DATA_NONE)
  {
    eventDataCancel(opaque, id, LG_CLIPBOARD_CANCEL_ABORTED);
    return;
  }

  LG_LOCK(clipboard.callbackLock);
  ClipboardBinding current;
  ClipboardRequest * request = takeIncoming(binding, id, &current);
  if (!request)
  {
    LG_UNLOCK(clipboard.callbackLock);
    DEBUG_WARN("Ignoring stale clipboard data");
    return;
  }

  bool terminal = false;
  LG_ClipboardResult result = LG_CLIPBOARD_RESULT_FAILED;
  if (type != request->type || !validType(type) || (size && !data) ||
      request->started || request->compat)
  {
    cancelRequest(request, LG_CLIPBOARD_CANCEL_INVALID);
    terminal = true;
  }
  else if (request->replyFn && size <= UINT32_MAX)
  {
    request->replyFn(request->opaque, type, data, (uint32_t)size);
    terminal = true;
    result = LG_CLIPBOARD_RESULT_ACCEPTED;
  }
  else if (!request->replyFn)
  {
    if (size)
    {
      request->compatData = malloc(size);
      if (request->compatData)
        memcpy(request->compatData, data, size);
    }
    if (!size || request->compatData)
    {
      request->compat      = true;
      request->compatPhase = COMPAT_BEGIN;
      request->compatType  = type;
      request->compatSize  = size;
      result = pumpIncomingCompat(request, &terminal);
    }
    else
    {
      cancelRequest(request, LG_CLIPBOARD_CANCEL_UNAVAILABLE);
      terminal = true;
    }
  }
  else
  {
    cancelRequest(request, LG_CLIPBOARD_CANCEL_INVALID);
    terminal = true;
  }

  if (!terminal && !returnRequest(request))
    result = LG_CLIPBOARD_RESULT_FAILED;
  if (terminal)
    freeRequest(request);
  LG_UNLOCK(clipboard.callbackLock);

  if (result == LG_CLIPBOARD_RESULT_FAILED)
    DEBUG_ERROR("Invalid clipboard response from %s", current.ops->name);
}

static void eventRelease(void * opaque)
{
  ClipboardBinding * binding = opaque;

  LG_LOCK_SHARED(clipboard.activeLock);
  if (!bindingActiveNL(binding))
  {
    LG_UNLOCK_SHARED(clipboard.activeLock);
    return;
  }
  LG_UNLOCK_SHARED(clipboard.activeLock);

  bool release;
  LG_ClipboardRequest transfer;
  LG_LOCK(clipboard.requestLock);
  LG_LOCK(clipboard.stateLock);
  release = clipboard.remoteNotice && clipboard.localAvailable &&
    g_params.clipboardToLocal;
  clipboard.remoteNotice      = false;
  clipboard.remoteType        = LG_CLIPBOARD_DATA_NONE;
  clipboard.remoteFileDataset = 0;
  clipboard.remoteGeneration  = nextGeneration(
      clipboard.remoteGeneration);
  transfer = clearRemoteRequestNL();
  LG_UNLOCK(clipboard.stateLock);
  LG_UNLOCK(clipboard.requestLock);

  LG_LOCK(clipboard.fileLock);
  clipboardFiles_remoteClear();
  LG_UNLOCK(clipboard.fileLock);

  LG_LOCK(clipboard.callbackLock);
  cancelRequestsNL(NULL, 0, true, LG_CLIPBOARD_CANCEL_REPLACED);
  if (transfer != LG_CLIPBOARD_REQUEST_INVALID &&
      g_state.ds->cbRequestCancel)
    g_state.ds->cbRequestCancel(transfer, LG_CLIPBOARD_CANCEL_REPLACED);
  LG_LOCK(clipboard.stateLock);
  release = release && clipboard.localAvailable &&
    !clipboard.remoteNotice;
  LG_UNLOCK(clipboard.stateLock);
  if (release)
    g_state.ds->cbRelease();
  LG_UNLOCK(clipboard.callbackLock);
}

static bool eventRequest(void * opaque, LG_ClipboardRequest id,
    LG_ClipboardData type)
{
  ClipboardBinding * binding = opaque;
  if (id == LG_CLIPBOARD_REQUEST_INVALID || !validType(type))
    return false;

  LG_LOCK_SHARED(clipboard.activeLock);
  if (!bindingActiveNL(binding))
  {
    LG_UNLOCK_SHARED(clipboard.activeLock);
    return false;
  }

  const ClipboardBinding current = *binding;
  const char * name = current.ops->name;
  LG_UNLOCK_SHARED(clipboard.activeLock);
  LG_ClipboardRequest transfer = LG_CLIPBOARD_REQUEST_INVALID;
  LG_LOCK(clipboard.stateLock);
  const bool request = clipboard.localAvailable &&
    g_params.clipboardToVM && localHasTypeNL(type) &&
    !clipboard.remoteRequest;
  if (request)
  {
    clipboard.remoteRequest        = true;
    clipboard.remoteRequestBinding = current;
    clipboard.remoteRequestId      = id;
    clipboard.remoteTransferId     = nextTransferNL();
    clipboard.remoteRequestType    = type;
    transfer = clipboard.remoteTransferId;
  }
  LG_UNLOCK(clipboard.stateLock);

  LG_LOCK(clipboard.callbackLock);
  LG_LOCK(clipboard.stateLock);
  const bool deliver = request && clipboard.localAvailable &&
    clipboard.remoteRequest &&
    clipboard.remoteRequestId == id &&
    clipboard.remoteTransferId == transfer;
  LG_UNLOCK(clipboard.stateLock);
  LG_LOCK_SHARED(clipboard.activeLock);
  const bool active = bindingActiveNL(&current);
  LG_UNLOCK_SHARED(clipboard.activeLock);
  if (deliver && active)
    g_state.ds->cbRequest(transfer, type);
  else
  {
    LG_LOCK(clipboard.stateLock);
    if (clipboard.remoteRequest &&
        clipboard.remoteRequestId == id &&
        clipboard.remoteTransferId == transfer)
      clearRemoteRequestNL();
    LG_UNLOCK(clipboard.stateLock);
    DEBUG_WARN("Ignoring invalid clipboard request from %s",
        name);
  }
  LG_UNLOCK(clipboard.callbackLock);
  return deliver && active;
}

static void eventDataReady(void * opaque, LG_ClipboardRequest id)
{
  ClipboardBinding * binding = opaque;
  LG_LOCK(clipboard.callbackLock);
  LG_LOCK(clipboard.writeLock);
  LG_LOCK_SHARED(clipboard.activeLock);
  LG_LOCK(clipboard.stateLock);
  const bool valid = bindingActiveNL(binding) && clipboard.remoteRequest &&
    clipboard.remoteRequestId == id && clipboard.remoteBlocked &&
    bindingEqual(&clipboard.remoteRequestBinding, &clipboard.active);
  const bool compat = valid && clipboard.remoteCompat;
  const LG_ClipboardRequest transfer = valid ?
    clipboard.remoteTransferId : LG_CLIPBOARD_REQUEST_INVALID;
  if (valid)
    clipboard.remoteBlocked = false;
  LG_UNLOCK(clipboard.stateLock);

  if (compat)
    pumpRemoteCompat();
  LG_UNLOCK_SHARED(clipboard.activeLock);
  LG_UNLOCK(clipboard.writeLock);
  if (!compat && valid && g_state.ds->cbRequestReady)
    g_state.ds->cbRequestReady(transfer);
  LG_UNLOCK(clipboard.callbackLock);
}

static void eventRequestCancel(void * opaque, LG_ClipboardRequest id,
    LG_ClipboardCancelReason reason)
{
  ClipboardBinding * binding = opaque;
  LG_LOCK(clipboard.callbackLock);
  LG_LOCK(clipboard.writeLock);
  LG_LOCK_SHARED(clipboard.activeLock);
  LG_LOCK(clipboard.stateLock);
  const bool valid = bindingActiveNL(binding) && clipboard.remoteRequest &&
    clipboard.remoteRequestId == id &&
    bindingEqual(&clipboard.remoteRequestBinding, &clipboard.active);
  const LG_ClipboardRequest transfer = valid ?
    clipboard.remoteTransferId : LG_CLIPBOARD_REQUEST_INVALID;
  if (valid)
    clearRemoteRequestNL();
  LG_UNLOCK(clipboard.stateLock);
  LG_UNLOCK_SHARED(clipboard.activeLock);
  LG_UNLOCK(clipboard.writeLock);

  if (valid && g_state.ds->cbRequestCancel)
    g_state.ds->cbRequestCancel(transfer, reason);
  LG_UNLOCK(clipboard.callbackLock);
}

static bool fileEventBindingActive(void * opaque)
{
  ClipboardBinding * binding = opaque;
  LG_LOCK_SHARED(clipboard.activeLock);
  const bool active = bindingActiveNL(binding);
  LG_UNLOCK_SHARED(clipboard.activeLock);
  return active;
}

static void eventFileOffer(void * opaque, uint64_t dataset)
{
  LG_LOCK(clipboard.fileLock);
  const bool active = fileEventBindingActive(opaque);
  if (!active || !dataset)
  {
    LG_UNLOCK(clipboard.fileLock);
    return;
  }

  LG_LOCK(clipboard.stateLock);
  clipboard.remoteFileDataset = dataset;
  LG_UNLOCK(clipboard.stateLock);
  if (!clipboardFiles_remoteOffer(dataset))
  {
    LG_LOCK(clipboard.stateLock);
    if (clipboard.remoteFileDataset == dataset)
      clipboard.remoteFileDataset = 0;
    LG_UNLOCK(clipboard.stateLock);
    clipboardFiles_remoteClear();
  }
  LG_UNLOCK(clipboard.fileLock);
}

static void eventFileAcquire(void * opaque, uint64_t dataset,
    uint64_t acquisition)
{
  LG_LOCK(clipboard.fileLock);
  if (!fileEventBindingActive(opaque))
  {
    LG_UNLOCK(clipboard.fileLock);
    return;
  }
  clipboardFiles_localAcquire(dataset, acquisition);
  LG_UNLOCK(clipboard.fileLock);
}

static void eventFileAcquired(void * opaque, uint64_t dataset,
    uint64_t acquisition, LG_ClipboardFileError error)
{
  LG_LOCK(clipboard.fileLock);
  if (!fileEventBindingActive(opaque))
  {
    LG_UNLOCK(clipboard.fileLock);
    return;
  }
  clipboardFiles_remoteAcquired(dataset, acquisition, error);
  LG_UNLOCK(clipboard.fileLock);
}

static void eventFileRelease(void * opaque, uint64_t dataset,
    uint64_t acquisition)
{
  LG_LOCK(clipboard.fileLock);
  if (!fileEventBindingActive(opaque))
  {
    LG_UNLOCK(clipboard.fileLock);
    return;
  }
  clipboardFiles_localRelease(dataset, acquisition);
  LG_UNLOCK(clipboard.fileLock);
}

static void eventFileRequest(void * opaque,
    const LG_ClipboardFileRequest * request)
{
  LG_LOCK(clipboard.fileLock);
  if (!fileEventBindingActive(opaque))
  {
    LG_UNLOCK(clipboard.fileLock);
    return;
  }
  clipboardFiles_localRequest(request);
  LG_UNLOCK(clipboard.fileLock);
}

static LG_ClipboardResult eventFileDataBegin(void * opaque,
    const LG_ClipboardFileRequest * request, uint64_t sizeHint)
{
  LG_LOCK(clipboard.fileLock);
  if (!fileEventBindingActive(opaque))
  {
    LG_UNLOCK(clipboard.fileLock);
    return LG_CLIPBOARD_RESULT_FAILED;
  }
  const LG_ClipboardResult result =
    clipboardFiles_remoteDataBegin(request, sizeHint);
  LG_UNLOCK(clipboard.fileLock);
  return result;
}

static LG_ClipboardResult eventFileDataChunk(void * opaque,
    const LG_ClipboardFileRequest * request, uint64_t responseOffset,
    const void * data, size_t size)
{
  LG_LOCK(clipboard.fileLock);
  if (!fileEventBindingActive(opaque))
  {
    LG_UNLOCK(clipboard.fileLock);
    return LG_CLIPBOARD_RESULT_FAILED;
  }
  const LG_ClipboardResult result = clipboardFiles_remoteDataChunk(
      request, responseOffset, data, size);
  LG_UNLOCK(clipboard.fileLock);
  return result;
}

static LG_ClipboardResult eventFileDataEnd(void * opaque,
    const LG_ClipboardFileRequest * request, uint64_t finalSize)
{
  LG_LOCK(clipboard.fileLock);
  if (!fileEventBindingActive(opaque))
  {
    LG_UNLOCK(clipboard.fileLock);
    return LG_CLIPBOARD_RESULT_FAILED;
  }
  const LG_ClipboardResult result =
    clipboardFiles_remoteDataEnd(request, finalSize);
  LG_UNLOCK(clipboard.fileLock);
  return result;
}

static void eventFileDataReady(void * opaque, uint64_t request)
{
  LG_LOCK(clipboard.fileLock);
  if (!fileEventBindingActive(opaque))
  {
    LG_UNLOCK(clipboard.fileLock);
    return;
  }
  clipboardFiles_localReady(request);
  LG_UNLOCK(clipboard.fileLock);
}

static void eventFileCancel(void * opaque, uint64_t dataset,
    uint64_t request, LG_ClipboardFileError reason)
{
  LG_LOCK(clipboard.fileLock);
  if (!fileEventBindingActive(opaque))
  {
    LG_UNLOCK(clipboard.fileLock);
    return;
  }
  clipboardFiles_remoteCancel(dataset, request, reason);
  clipboardFiles_localCancel(dataset, request, reason);
  LG_UNLOCK(clipboard.fileLock);
}

static const LG_ClipboardEventOps eventOps =
{
  .notice        = eventNotice,
  .data          = eventData,
  .dataBegin     = eventDataBegin,
  .dataChunk     = eventDataChunk,
  .dataEnd       = eventDataEnd,
  .dataCancel    = eventDataCancel,
  .dataReady     = eventDataReady,
  .requestCancel = eventRequestCancel,
  .release       = eventRelease,
  .request       = eventRequest,
  .fileOffer     = eventFileOffer,
  .fileAcquire   = eventFileAcquire,
  .fileAcquired  = eventFileAcquired,
  .fileRelease   = eventFileRelease,
  .fileRequest   = eventFileRequest,
  .fileDataBegin = eventFileDataBegin,
  .fileDataChunk = eventFileDataChunk,
  .fileDataEnd   = eventFileDataEnd,
  .fileDataReady = eventFileDataReady,
  .fileCancel    = eventFileCancel,
};

/* providerLock must be held. dropActive suppresses all calls into an endpoint
 * which has already disappeared. */
static void updateActive(bool dropActive)
{
  for (;;)
  {
    LG_LOCK_EXCLUSIVE(clipboard.activeLock);
    ClipboardBinding * slot = nextBindingSlotNL();
    ClipboardBinding next = slot ? *slot : (ClipboardBinding) { 0 };
    const ClipboardBinding old = clipboard.active;
    if (bindingEqual(&old, &next))
    {
      clipboard.active = next;
      LG_UNLOCK_EXCLUSIVE(clipboard.activeLock);
      return;
    }
    clipboard.active = (ClipboardBinding) { 0 };
    LG_UNLOCK_EXCLUSIVE(clipboard.activeLock);

    if (old.ops && !dropActive)
    {
      if (!old.ops->release(old.opaque))
        DEBUG_WARN("Failed to release Clipboard provider: %s",
            old.ops->name);
      old.ops->detach(old.opaque);
    }
    dropActive = false;
    resetRemote();

    LG_LOCK_EXCLUSIVE(clipboard.activeLock);
    slot = nextBindingSlotNL();
    if (!slot)
    {
      LG_UNLOCK_EXCLUSIVE(clipboard.activeLock);
      DEBUG_INFO("Clipboard is unavailable");
      return;
    }
    next = *slot;
    clipboard.active = next;
    LG_UNLOCK_EXCLUSIVE(clipboard.activeLock);

    if (next.ops->attach(next.opaque, &eventOps, slot))
    {
      publishLocal(&next);
      DEBUG_INFO("Using Clipboard: %s", next.ops->name);
      return;
    }

    next.ops->detach(next.opaque);
    resetRemote();

    LG_LOCK_EXCLUSIVE(clipboard.activeLock);
    if (bindingEqual(&clipboard.active, &next))
      clipboard.active = (ClipboardBinding) { 0 };
    if (bindingEqual(slot, &next))
      slot->available = false;
    LG_UNLOCK_EXCLUSIVE(clipboard.activeLock);

    DEBUG_WARN("Failed to attach Clipboard provider: %s", next.ops->name);
  }
}

static void statusChanged(ClipboardBinding * binding, void * opaque,
    const LG_ClipboardStatus * status)
{
  if (!status)
    return;

  LG_LOCK(clipboard.providerLock);
  LG_LOCK_EXCLUSIVE(clipboard.activeLock);
  const bool current = binding->ops && binding->opaque == opaque;
  const bool wasActive = current && bindingActiveNL(binding);
  const bool changed = current &&
    (binding->available != status->available ||
     binding->generation != status->generation);
  if (changed)
  {
    binding->available  = status->available;
    binding->generation = status->generation;
  }
  LG_UNLOCK_EXCLUSIVE(clipboard.activeLock);
  if (changed)
    updateActive(wasActive && !status->available);
  LG_UNLOCK(clipboard.providerLock);
}

static void fallbackStatusChanged(void * opaque,
    const LG_ClipboardStatus * status)
{
  statusChanged(&clipboard.fallback, opaque, status);
}

static void transportStatusChanged(void * opaque,
    const LG_ClipboardStatus * status)
{
  statusChanged(&clipboard.transport, opaque, status);
}

static void setBinding(ClipboardBinding * target,
    const LG_ClipboardOps * ops, void * opaque,
    LG_ClipboardStatusFn statusFn)
{
  if (ops && !validOps(ops))
  {
    DEBUG_ERROR("Invalid clipboard operations");
    ops    = NULL;
    opaque = NULL;
  }

  LG_LOCK(clipboard.registrationLock);
  LG_LOCK(clipboard.providerLock);
  const ClipboardBinding old = *target;
  LG_UNLOCK(clipboard.providerLock);

  if (old.ops && old.ops->setStatusListener)
    old.ops->setStatusListener(old.opaque, NULL, NULL);

  const ClipboardBinding next = makeBinding(ops, opaque);
  LG_LOCK(clipboard.providerLock);
  LG_LOCK_EXCLUSIVE(clipboard.activeLock);
  *target = next;
  LG_UNLOCK_EXCLUSIVE(clipboard.activeLock);
  updateActive(false);
  LG_UNLOCK(clipboard.providerLock);

  if (next.ops && next.ops->setStatusListener)
    next.ops->setStatusListener(next.opaque, statusFn, next.opaque);
  LG_UNLOCK(clipboard.registrationLock);
}

bool clipboard_init(void)
{
  memset(&clipboard, 0, sizeof(clipboard));
  LG_LOCK_INIT(clipboard.registrationLock);
  LG_LOCK_INIT(clipboard.providerLock);
  LG_RWLOCK_INIT(clipboard.activeLock);
  LG_LOCK_INIT(clipboard.stateLock);
  LG_LOCK_INIT(clipboard.requestLock);
  LG_LOCK_INIT(clipboard.writeLock);
  LG_LOCK_INIT(clipboard.callbackLock);
  LG_LOCK_INIT(clipboard.fileLock);
  clipboard.remoteType        = LG_CLIPBOARD_DATA_NONE;
  clipboard.remoteRequestType = LG_CLIPBOARD_DATA_NONE;
  clipboard.requests          = ll_new();
  if (!clipboardFiles_init())
  {
    DEBUG_ERROR("Failed to initialize clipboard file transfer");
    return false;
  }
  return true;
}

void clipboard_free(void)
{
  clipboard_setLocalAvailable(false);
  clipboard_setTransport(NULL, NULL);
  clipboard_setFallback(NULL, NULL);
  clipboardFiles_free();
  LG_LOCK(clipboard.callbackLock);
  cancelRequestsNL(NULL, 0, true, LG_CLIPBOARD_CANCEL_UNAVAILABLE);
  LG_UNLOCK(clipboard.callbackLock);
  if (clipboard.requests)
  {
    ll_free(clipboard.requests);
    clipboard.requests = NULL;
  }

  LG_LOCK_FREE(clipboard.writeLock);
  LG_LOCK_FREE(clipboard.requestLock);
  LG_LOCK_FREE(clipboard.stateLock);
  LG_LOCK_FREE(clipboard.callbackLock);
  LG_LOCK_FREE(clipboard.fileLock);
  LG_RWLOCK_FREE(clipboard.activeLock);
  LG_LOCK_FREE(clipboard.providerLock);
  LG_LOCK_FREE(clipboard.registrationLock);
}

void clipboard_setLocalAvailable(bool available)
{
  bool notice;
  bool release;
  LG_ClipboardData type;
  uint64_t fileDataset;
  LG_ClipboardRequest transfer = LG_CLIPBOARD_REQUEST_INVALID;

  LG_LOCK(clipboard.callbackLock);
  LG_LOCK(clipboard.requestLock);
  LG_LOCK(clipboard.stateLock);
  release = !available && clipboard.localAvailable &&
    clipboard.remoteNotice && g_params.clipboardToLocal;
  clipboard.localAvailable = available;
  notice = available && clipboard.remoteNotice &&
    g_params.clipboardToLocal;
  type = clipboard.remoteType;
  fileDataset = clipboard.remoteFileDataset;
  if (!available)
    transfer = clearRemoteRequestNL();
  LG_UNLOCK(clipboard.stateLock);
  LG_UNLOCK(clipboard.requestLock);

  if (notice)
  {
    if (type != LG_CLIPBOARD_DATA_FILES ||
        clipboardFiles_remoteReady(fileDataset))
      g_state.ds->cbNotice(type);
  }
  if (!available)
  {
    cancelRequestsNL(NULL, 0, true, LG_CLIPBOARD_CANCEL_UNAVAILABLE);
    if (transfer != LG_CLIPBOARD_REQUEST_INVALID &&
        g_state.ds->cbRequestCancel)
      g_state.ds->cbRequestCancel(
          transfer, LG_CLIPBOARD_CANCEL_UNAVAILABLE);
    if (release)
      g_state.ds->cbRelease();
  }
  LG_UNLOCK(clipboard.callbackLock);
}

void clipboard_setFallback(const LG_ClipboardOps * ops, void * opaque)
{
  setBinding(&clipboard.fallback,
      ops, opaque, fallbackStatusChanged);
}

static void dropBinding(ClipboardBinding * target)
{
  LG_LOCK(clipboard.registrationLock);
  LG_LOCK(clipboard.providerLock);
  LG_LOCK_EXCLUSIVE(clipboard.activeLock);
  const bool wasActive = bindingActiveNL(target);
  *target = (ClipboardBinding) { 0 };
  LG_UNLOCK_EXCLUSIVE(clipboard.activeLock);
  updateActive(wasActive);
  LG_UNLOCK(clipboard.providerLock);
  LG_UNLOCK(clipboard.registrationLock);
}

void clipboard_dropFallback(void)
{
  dropBinding(&clipboard.fallback);
}

void clipboard_setTransport(const LG_ClipboardOps * ops, void * opaque)
{
  setBinding(&clipboard.transport,
      ops, opaque, transportStatusChanged);
}

void clipboard_dropTransport(void)
{
  dropBinding(&clipboard.transport);
}

void clipboard_release(void)
{
  if (!g_params.clipboardToVM)
    return;

  LG_LOCK(clipboard.writeLock);
  LG_LOCK(clipboard.stateLock);
  clipboard.localTypeCount = 0;
  clipboard.localFileDataset = 0;
  clearRemoteRequestNL();
  LG_UNLOCK(clipboard.stateLock);

  LG_LOCK_SHARED(clipboard.activeLock);
  if (clipboard.active.ops &&
      !clipboard.active.ops->release(clipboard.active.opaque))
    DEBUG_WARN("Failed to release the remote clipboard");
  LG_UNLOCK_SHARED(clipboard.activeLock);
  LG_UNLOCK(clipboard.writeLock);
}

void clipboard_notifyTypes(
    const LG_ClipboardData types[], size_t count)
{
  if (!g_params.clipboardToVM)
    return;
  if (count == 0)
  {
    clipboard_release();
    return;
  }
  if (!types || count > LG_CLIPBOARD_DATA_NONE)
    return;

  for (size_t i = 0; i < count; ++i)
    if (!validType(types[i]) || types[i] == LG_CLIPBOARD_DATA_FILES)
      return;

  LG_LOCK(clipboard.writeLock);
  LG_LOCK(clipboard.stateLock);
  memcpy(clipboard.localTypes, types, count * sizeof(*types));
  clipboard.localTypeCount = count;
  clipboard.localFileDataset = 0;
  clearRemoteRequestNL();
  LG_UNLOCK(clipboard.stateLock);

  LG_LOCK_SHARED(clipboard.activeLock);
  if (clipboard.active.ops &&
      !clipboard.active.ops->notifyTypes(
        clipboard.active.opaque, types, count))
    DEBUG_WARN("Failed to publish the local clipboard types");
  LG_UNLOCK_SHARED(clipboard.activeLock);
  LG_UNLOCK(clipboard.writeLock);
}

void clipboard_notifyFiles(uint64_t dataset)
{
  if (!g_params.clipboardToVM || !dataset)
    return;

  LG_LOCK(clipboard.writeLock);
  LG_LOCK(clipboard.stateLock);
  clipboard.localTypes[0] = LG_CLIPBOARD_DATA_FILES;
  clipboard.localTypeCount = 1;
  clipboard.localFileDataset = dataset;
  clearRemoteRequestNL();
  LG_UNLOCK(clipboard.stateLock);

  LG_LOCK_SHARED(clipboard.activeLock);
  const bool result = !clipboard.active.ops ||
    (clipboard.active.ops->offerFiles ?
      clipboard.active.ops->offerFiles(clipboard.active.opaque, dataset) :
      clipboard.active.ops->release(clipboard.active.opaque));
  if (!result)
    DEBUG_WARN("Failed to publish the local clipboard file dataset");
  LG_UNLOCK_SHARED(clipboard.activeLock);
  LG_UNLOCK(clipboard.writeLock);
}

/* writeLock and activeLock must be held. */
static bool cancelRemoteNL(LG_ClipboardCancelReason reason)
{
  const bool result = streamProvider(clipboard.active.ops) ?
    clipboard.active.ops->dataCancel(clipboard.active.opaque,
        clipboard.remoteRequestId, reason) :
    clipboard.active.ops->data(clipboard.active.opaque,
        clipboard.remoteRequestId, LG_CLIPBOARD_DATA_NONE, NULL, 0);
  clearRemoteRequestNL();
  return result;
}

/* writeLock and activeLock must be held. */
static LG_ClipboardResult sendBegin(LG_ClipboardRequest transfer,
    LG_ClipboardData type, uint64_t sizeHint)
{
  LG_LOCK(clipboard.stateLock);
  const bool matches = clipboard.remoteRequest &&
    clipboard.remoteTransferId == transfer && clipboard.active.ops &&
    bindingEqual(&clipboard.remoteRequestBinding, &clipboard.active);
  if (!matches)
  {
    LG_UNLOCK(clipboard.stateLock);
    return LG_CLIPBOARD_RESULT_FAILED;
  }
  if (clipboard.remoteBlocked)
  {
    LG_UNLOCK(clipboard.stateLock);
    return LG_CLIPBOARD_RESULT_BLOCKED;
  }
  if (clipboard.remoteStarted || clipboard.remoteRequestType != type ||
      !validType(type))
  {
    cancelRemoteNL(LG_CLIPBOARD_CANCEL_INVALID);
    LG_UNLOCK(clipboard.stateLock);
    return LG_CLIPBOARD_RESULT_FAILED;
  }

  LG_ClipboardResult result;
  if (streamProvider(clipboard.active.ops))
    result = clipboard.active.ops->dataBegin(clipboard.active.opaque,
        clipboard.remoteRequestId, type, sizeHint);
  else if (sizeHint != LG_CLIPBOARD_SIZE_UNKNOWN && sizeHint > SIZE_MAX)
    result = LG_CLIPBOARD_RESULT_FAILED;
  else
  {
    free(clipboard.remoteBuffer);
    clipboard.remoteBuffer         = NULL;
    clipboard.remoteBufferSize     = 0;
    clipboard.remoteBufferCapacity = 0;
    result = LG_CLIPBOARD_RESULT_ACCEPTED;
  }

  if (result == LG_CLIPBOARD_RESULT_ACCEPTED)
  {
    clipboard.remoteStarted  = true;
    clipboard.remoteSizeHint = sizeHint;
    clipboard.remoteOffset   = 0;
  }
  else if (result == LG_CLIPBOARD_RESULT_BLOCKED)
    clipboard.remoteBlocked = true;
  else
    clearRemoteRequestNL();
  LG_UNLOCK(clipboard.stateLock);
  return result;
}

/* writeLock and activeLock must be held. */
static LG_ClipboardResult sendChunk(LG_ClipboardRequest transfer,
    uint64_t offset, const void * data, size_t size)
{
  LG_LOCK(clipboard.stateLock);
  const bool matches = clipboard.remoteRequest &&
    clipboard.remoteTransferId == transfer && clipboard.active.ops &&
    bindingEqual(&clipboard.remoteRequestBinding, &clipboard.active);
  if (!matches)
  {
    LG_UNLOCK(clipboard.stateLock);
    return LG_CLIPBOARD_RESULT_FAILED;
  }
  if (clipboard.remoteBlocked)
  {
    LG_UNLOCK(clipboard.stateLock);
    return LG_CLIPBOARD_RESULT_BLOCKED;
  }
  if (!clipboard.remoteStarted || clipboard.remoteOffset != offset ||
      !data || !size || size > UINT64_MAX - offset)
  {
    cancelRemoteNL(LG_CLIPBOARD_CANCEL_INVALID);
    LG_UNLOCK(clipboard.stateLock);
    return LG_CLIPBOARD_RESULT_FAILED;
  }

  LG_ClipboardResult result;
  if (streamProvider(clipboard.active.ops))
    result = clipboard.active.ops->dataChunk(clipboard.active.opaque,
        clipboard.remoteRequestId, offset, data, size);
  else if (offset > SIZE_MAX || size > SIZE_MAX - (size_t)offset ||
      !growBuffer(&clipboard.remoteBuffer,
        &clipboard.remoteBufferCapacity, (size_t)offset + size))
    result = LG_CLIPBOARD_RESULT_FAILED;
  else
  {
    memcpy(clipboard.remoteBuffer + offset, data, size);
    clipboard.remoteBufferSize = (size_t)offset + size;
    result = LG_CLIPBOARD_RESULT_ACCEPTED;
  }

  if (result == LG_CLIPBOARD_RESULT_ACCEPTED)
    clipboard.remoteOffset += size;
  else if (result == LG_CLIPBOARD_RESULT_BLOCKED)
    clipboard.remoteBlocked = true;
  else
    clearRemoteRequestNL();
  LG_UNLOCK(clipboard.stateLock);
  return result;
}

/* writeLock and activeLock must be held. */
static LG_ClipboardResult sendEnd(LG_ClipboardRequest transfer,
    uint64_t finalSize)
{
  LG_LOCK(clipboard.stateLock);
  const bool matches = clipboard.remoteRequest &&
    clipboard.remoteTransferId == transfer && clipboard.active.ops &&
    bindingEqual(&clipboard.remoteRequestBinding, &clipboard.active);
  if (!matches)
  {
    LG_UNLOCK(clipboard.stateLock);
    return LG_CLIPBOARD_RESULT_FAILED;
  }
  if (clipboard.remoteBlocked)
  {
    LG_UNLOCK(clipboard.stateLock);
    return LG_CLIPBOARD_RESULT_BLOCKED;
  }
  if (!clipboard.remoteStarted || clipboard.remoteOffset != finalSize)
  {
    cancelRemoteNL(LG_CLIPBOARD_CANCEL_INVALID);
    LG_UNLOCK(clipboard.stateLock);
    return LG_CLIPBOARD_RESULT_FAILED;
  }

  LG_ClipboardResult result;
  if (streamProvider(clipboard.active.ops))
    result = clipboard.active.ops->dataEnd(clipboard.active.opaque,
        clipboard.remoteRequestId, finalSize);
  else
    result = clipboard.active.ops->data(clipboard.active.opaque,
        clipboard.remoteRequestId, clipboard.remoteRequestType,
        clipboard.remoteBufferSize ? clipboard.remoteBuffer : NULL,
        clipboard.remoteBufferSize) ?
      LG_CLIPBOARD_RESULT_ACCEPTED : LG_CLIPBOARD_RESULT_FAILED;

  if (result == LG_CLIPBOARD_RESULT_BLOCKED)
    clipboard.remoteBlocked = true;
  else
    clearRemoteRequestNL();
  LG_UNLOCK(clipboard.stateLock);
  return result;
}

/* writeLock and activeLock must be held. */
static bool sendCancel(LG_ClipboardRequest transfer,
    LG_ClipboardCancelReason reason)
{
  LG_LOCK(clipboard.stateLock);
  const bool valid = clipboard.remoteRequest &&
    clipboard.remoteTransferId == transfer && clipboard.active.ops &&
    bindingEqual(&clipboard.remoteRequestBinding, &clipboard.active);
  if (!valid)
  {
    LG_UNLOCK(clipboard.stateLock);
    return false;
  }

  const bool result = cancelRemoteNL(reason);
  LG_UNLOCK(clipboard.stateLock);
  return result;
}

static LG_ClipboardResult pumpRemoteCompat(void)
{
  for (;;)
  {
    LG_LOCK(clipboard.stateLock);
    if (!clipboard.remoteRequest || !clipboard.remoteCompat)
    {
      LG_UNLOCK(clipboard.stateLock);
      return LG_CLIPBOARD_RESULT_FAILED;
    }
    const LG_ClipboardRequest transfer = clipboard.remoteTransferId;
    const LG_ClipboardData type = clipboard.remoteRequestType;
    const unsigned int phase = clipboard.remoteCompatPhase;
    const uint64_t offset = clipboard.remoteOffset;
    const size_t total = clipboard.remoteBufferSize;
    const uint8_t * data = clipboard.remoteBuffer;
    LG_UNLOCK(clipboard.stateLock);

    LG_ClipboardResult result;
    switch (phase)
    {
      case COMPAT_BEGIN:
        result = sendBegin(transfer, type, total);
        if (result != LG_CLIPBOARD_RESULT_ACCEPTED)
          return result;
        LG_LOCK(clipboard.stateLock);
        if (clipboard.remoteRequest)
          clipboard.remoteCompatPhase = COMPAT_CHUNK;
        LG_UNLOCK(clipboard.stateLock);
        break;

      case COMPAT_CHUNK:
        if (offset < total)
        {
          const size_t remaining = total - offset;
          const size_t size = remaining < COMPAT_CHUNK_SIZE ?
            remaining : COMPAT_CHUNK_SIZE;
          result = sendChunk(transfer, offset, data + offset, size);
          if (result != LG_CLIPBOARD_RESULT_ACCEPTED)
            return result;
          break;
        }
        LG_LOCK(clipboard.stateLock);
        if (clipboard.remoteRequest)
          clipboard.remoteCompatPhase = COMPAT_END;
        LG_UNLOCK(clipboard.stateLock);
        break;

      case COMPAT_END:
        return sendEnd(transfer, total);

      default:
        sendCancel(transfer, LG_CLIPBOARD_CANCEL_INVALID);
        return LG_CLIPBOARD_RESULT_FAILED;
    }
  }
}

LG_ClipboardResult clipboard_dataBegin(LG_ClipboardRequest transfer,
    LG_ClipboardData type, uint64_t sizeHint)
{
  if (!g_params.clipboardToVM ||
      transfer == LG_CLIPBOARD_REQUEST_INVALID)
    return LG_CLIPBOARD_RESULT_FAILED;

  LG_LOCK(clipboard.writeLock);
  LG_LOCK_SHARED(clipboard.activeLock);
  LG_LOCK(clipboard.stateLock);
  const bool compat = clipboard.remoteCompat;
  LG_UNLOCK(clipboard.stateLock);
  const LG_ClipboardResult result = compat ?
    LG_CLIPBOARD_RESULT_FAILED : sendBegin(transfer, type, sizeHint);
  LG_UNLOCK_SHARED(clipboard.activeLock);
  LG_UNLOCK(clipboard.writeLock);
  return result;
}

LG_ClipboardResult clipboard_dataChunk(LG_ClipboardRequest transfer,
    uint64_t offset, const void * data, size_t size)
{
  if (!g_params.clipboardToVM ||
      transfer == LG_CLIPBOARD_REQUEST_INVALID)
    return LG_CLIPBOARD_RESULT_FAILED;

  LG_LOCK(clipboard.writeLock);
  LG_LOCK_SHARED(clipboard.activeLock);
  LG_LOCK(clipboard.stateLock);
  const bool compat = clipboard.remoteCompat;
  LG_UNLOCK(clipboard.stateLock);
  const LG_ClipboardResult result = compat ?
    LG_CLIPBOARD_RESULT_FAILED : sendChunk(transfer, offset, data, size);
  LG_UNLOCK_SHARED(clipboard.activeLock);
  LG_UNLOCK(clipboard.writeLock);
  return result;
}

LG_ClipboardResult clipboard_dataEnd(LG_ClipboardRequest transfer,
    uint64_t finalSize)
{
  if (!g_params.clipboardToVM ||
      transfer == LG_CLIPBOARD_REQUEST_INVALID)
    return LG_CLIPBOARD_RESULT_FAILED;

  LG_LOCK(clipboard.writeLock);
  LG_LOCK_SHARED(clipboard.activeLock);
  LG_LOCK(clipboard.stateLock);
  const bool compat = clipboard.remoteCompat;
  LG_UNLOCK(clipboard.stateLock);
  const LG_ClipboardResult result = compat ?
    LG_CLIPBOARD_RESULT_FAILED : sendEnd(transfer, finalSize);
  LG_UNLOCK_SHARED(clipboard.activeLock);
  LG_UNLOCK(clipboard.writeLock);
  return result;
}

void clipboard_data(LG_ClipboardRequest transfer,
    LG_ClipboardData type, const void * data, size_t size)
{
  if (!g_params.clipboardToVM ||
      transfer == LG_CLIPBOARD_REQUEST_INVALID)
    return;

  LG_LOCK(clipboard.writeLock);
  LG_LOCK_SHARED(clipboard.activeLock);
  LG_LOCK(clipboard.stateLock);
  const bool matches = clipboard.remoteRequest &&
    clipboard.remoteTransferId == transfer;
  const bool provider = matches && clipboard.active.ops &&
    bindingEqual(&clipboard.remoteRequestBinding, &clipboard.active);
  const bool valid = provider && clipboard.remoteRequestType == type &&
    validType(type) && (!size || data) && !clipboard.remoteStarted &&
    !clipboard.remoteBlocked && !clipboard.remoteCompat;

  if (!valid)
  {
    LG_UNLOCK(clipboard.stateLock);
    if (provider)
      sendCancel(transfer, LG_CLIPBOARD_CANCEL_INVALID);
    LG_UNLOCK_SHARED(clipboard.activeLock);
    LG_UNLOCK(clipboard.writeLock);
    DEBUG_WARN("Ignoring unexpected local clipboard data");
    return;
  }

  if (!streamProvider(clipboard.active.ops))
  {
    const LG_ClipboardRequest request = clipboard.remoteRequestId;
    clearRemoteRequestNL();
    LG_UNLOCK(clipboard.stateLock);
    const bool result = clipboard.active.ops->data(clipboard.active.opaque,
        request, type, data, size);
    LG_UNLOCK_SHARED(clipboard.activeLock);
    LG_UNLOCK(clipboard.writeLock);
    if (!result)
      DEBUG_WARN("Failed to send remote clipboard data");
    return;
  }

  uint8_t * copy = size ? malloc(size) : NULL;
  if (size && !copy)
  {
    LG_UNLOCK(clipboard.stateLock);
    sendCancel(transfer, LG_CLIPBOARD_CANCEL_UNAVAILABLE);
    LG_UNLOCK_SHARED(clipboard.activeLock);
    LG_UNLOCK(clipboard.writeLock);
    DEBUG_ERROR("Out of memory");
    return;
  }
  if (size)
    memcpy(copy, data, size);
  clipboard.remoteBuffer         = copy;
  clipboard.remoteBufferSize     = size;
  clipboard.remoteBufferCapacity = size;
  clipboard.remoteCompat         = true;
  clipboard.remoteCompatPhase    = COMPAT_BEGIN;
  LG_UNLOCK(clipboard.stateLock);

  const LG_ClipboardResult result = pumpRemoteCompat();
  LG_UNLOCK_SHARED(clipboard.activeLock);
  LG_UNLOCK(clipboard.writeLock);
  if (result == LG_CLIPBOARD_RESULT_FAILED)
    DEBUG_WARN("Failed to send remote clipboard data");
}

void clipboard_abort(LG_ClipboardRequest transfer)
{
  if (!g_params.clipboardToVM ||
      transfer == LG_CLIPBOARD_REQUEST_INVALID)
    return;

  LG_LOCK(clipboard.writeLock);
  LG_LOCK_SHARED(clipboard.activeLock);
  const bool result = sendCancel(
      transfer, LG_CLIPBOARD_CANCEL_ABORTED);
  LG_UNLOCK_SHARED(clipboard.activeLock);
  LG_UNLOCK(clipboard.writeLock);

  if (!result)
    DEBUG_WARN("Failed to abort remote clipboard data");
}

static bool requestClipboard(LG_ClipboardData type,
    LG_ClipboardReplyFn replyFn, const LG_ClipboardStreamOps * stream,
    void * opaque, LG_ClipboardRequest * resultId)
{
  if (resultId)
    *resultId = LG_CLIPBOARD_REQUEST_INVALID;
  if (!g_params.clipboardToLocal || !validType(type) ||
      (!replyFn && !validStream(stream)) || !clipboard.requests)
    return false;

  ClipboardRequest * request = calloc(1, sizeof(*request));
  if (!request)
  {
    DEBUG_ERROR("Out of memory");
    return false;
  }

  LG_LOCK_SHARED(clipboard.activeLock);
  if (!clipboard.active.ops)
  {
    LG_UNLOCK_SHARED(clipboard.activeLock);
    freeRequest(request);
    return false;
  }

  LG_LOCK(clipboard.requestLock);
  LG_LOCK(clipboard.stateLock);
  const bool available = clipboard.localAvailable &&
    clipboard.remoteNotice && clipboard.remoteType == type;
  const uint32_t generation = clipboard.remoteGeneration;
  LG_ClipboardRequest id = LG_CLIPBOARD_REQUEST_INVALID;
  if (available)
  {
    id = nextRequestNL();
    *request = (ClipboardRequest)
    {
      .request          = id,
      .binding          = clipboard.active,
      .remoteGeneration = generation,
      .type             = type,
      .replyFn          = replyFn,
      .stream           = replyFn ? &legacyStream : stream,
      .opaque           = opaque,
      .sizeHint         = LG_CLIPBOARD_SIZE_UNKNOWN,
    };
  }
  LG_UNLOCK(clipboard.stateLock);

  const bool queued = available &&
    ll_push(clipboard.requests, request);
  /* Publish the request before the provider can make it visible to its
   * worker. A provider event may otherwise reach the stream sink before the
   * caller has learned which request it belongs to. */
  if (queued && resultId)
    *resultId = id;
  bool result = queued;
  if (queued)
    result = clipboard.active.ops->request(clipboard.active.opaque,
        request->request, type);
  LG_UNLOCK(clipboard.requestLock);
  LG_UNLOCK_SHARED(clipboard.activeLock);

  if (result)
    return true;
  if (queued)
  {
    ClipboardRequest * failed = takeRequest(id);
    if (!failed)
      return true;
    if (resultId)
      *resultId = LG_CLIPBOARD_REQUEST_INVALID;
    freeRequest(failed);
    return false;
  }

  freeRequest(request);
  return false;
}

bool clipboard_requestStream(LG_ClipboardData type,
    const LG_ClipboardStreamOps * stream, void * opaque,
    LG_ClipboardRequest * request)
{
  if (!request)
    return false;
  return requestClipboard(type, NULL, stream, opaque, request);
}

bool clipboard_requestReady(LG_ClipboardRequest id)
{
  if (id == LG_CLIPBOARD_REQUEST_INVALID || !clipboard.requests)
    return false;

  LG_LOCK(clipboard.callbackLock);
  ClipboardRequest * request = takeRequest(id);
  if (!request || !request->blocked)
  {
    if (request)
      returnRequest(request);
    LG_UNLOCK(clipboard.callbackLock);
    return false;
  }

  request->blocked = false;
  if (request->compat)
  {
    bool terminal = false;
    const LG_ClipboardResult result =
      pumpIncomingCompat(request, &terminal);
    if (!terminal && !returnRequest(request))
    {
      LG_UNLOCK(clipboard.callbackLock);
      return false;
    }
    if (terminal)
      freeRequest(request);
    LG_UNLOCK(clipboard.callbackLock);
    return result != LG_CLIPBOARD_RESULT_FAILED;
  }

  const ClipboardBinding binding = request->binding;
  if (!returnRequest(request))
  {
    LG_UNLOCK(clipboard.callbackLock);
    return false;
  }

  LG_LOCK_SHARED(clipboard.activeLock);
  const bool active = bindingActiveNL(&binding) &&
    streamProvider(binding.ops);
  const bool result = active && binding.ops->dataReady(
      binding.opaque, id);
  LG_UNLOCK_SHARED(clipboard.activeLock);
  if (!result)
  {
    ClipboardRequest * failed = takeRequest(id);
    if (failed)
    {
      cancelRequest(failed, LG_CLIPBOARD_CANCEL_UNAVAILABLE);
      freeRequest(failed);
    }
  }
  LG_UNLOCK(clipboard.callbackLock);
  return result;
}

bool clipboard_request(LG_ClipboardData type,
    LG_ClipboardReplyFn replyFn, void * opaque)
{
  if (!replyFn)
    return false;
  return requestClipboard(type, replyFn, NULL, opaque, NULL);
}

bool clipboard_fileAcquire(uint64_t dataset, uint64_t acquisition)
{
  bool result = false;
  LG_LOCK_SHARED(clipboard.activeLock);
  if (clipboard.active.ops && clipboard.active.ops->fileAcquire)
    result = clipboard.active.ops->fileAcquire(
        clipboard.active.opaque, dataset, acquisition);
  LG_UNLOCK_SHARED(clipboard.activeLock);
  return result;
}

bool clipboard_fileAcquired(uint64_t dataset, uint64_t acquisition,
    LG_ClipboardFileError error)
{
  bool result = false;
  LG_LOCK_SHARED(clipboard.activeLock);
  if (clipboard.active.ops && clipboard.active.ops->fileAcquired)
    result = clipboard.active.ops->fileAcquired(
        clipboard.active.opaque, dataset, acquisition, error);
  LG_UNLOCK_SHARED(clipboard.activeLock);
  return result;
}

bool clipboard_fileRelease(uint64_t dataset, uint64_t acquisition)
{
  bool result = false;
  LG_LOCK_SHARED(clipboard.activeLock);
  if (clipboard.active.ops && clipboard.active.ops->fileRelease)
    result = clipboard.active.ops->fileRelease(
        clipboard.active.opaque, dataset, acquisition);
  LG_UNLOCK_SHARED(clipboard.activeLock);
  return result;
}

bool clipboard_fileRequest(const LG_ClipboardFileRequest * request)
{
  bool result = false;
  LG_LOCK_SHARED(clipboard.activeLock);
  if (clipboard.active.ops && clipboard.active.ops->fileRequest)
    result = clipboard.active.ops->fileRequest(
        clipboard.active.opaque, request);
  LG_UNLOCK_SHARED(clipboard.activeLock);
  return result;
}

LG_ClipboardResult clipboard_fileDataBegin(
    const LG_ClipboardFileRequest * request, uint64_t sizeHint)
{
  LG_ClipboardResult result = LG_CLIPBOARD_RESULT_FAILED;
  LG_LOCK_SHARED(clipboard.activeLock);
  if (clipboard.active.ops && clipboard.active.ops->fileDataBegin)
    result = clipboard.active.ops->fileDataBegin(
        clipboard.active.opaque, request, sizeHint);
  LG_UNLOCK_SHARED(clipboard.activeLock);
  return result;
}

LG_ClipboardResult clipboard_fileDataChunk(
    const LG_ClipboardFileRequest * request, uint64_t responseOffset,
    const void * data, size_t size, bool end)
{
  LG_ClipboardResult result = LG_CLIPBOARD_RESULT_FAILED;
  LG_LOCK_SHARED(clipboard.activeLock);
  if (clipboard.active.ops && clipboard.active.ops->fileDataChunk)
    result = clipboard.active.ops->fileDataChunk(
        clipboard.active.opaque, request, responseOffset, data, size, end);
  LG_UNLOCK_SHARED(clipboard.activeLock);
  return result;
}

LG_ClipboardResult clipboard_fileDataEnd(
    const LG_ClipboardFileRequest * request, uint64_t finalSize)
{
  LG_ClipboardResult result = LG_CLIPBOARD_RESULT_FAILED;
  LG_LOCK_SHARED(clipboard.activeLock);
  if (clipboard.active.ops && clipboard.active.ops->fileDataEnd)
    result = clipboard.active.ops->fileDataEnd(
        clipboard.active.opaque, request, finalSize);
  LG_UNLOCK_SHARED(clipboard.activeLock);
  return result;
}

bool clipboard_fileCancel(uint64_t dataset, uint64_t request,
    LG_ClipboardFileError reason)
{
  bool result = false;
  LG_LOCK_SHARED(clipboard.activeLock);
  if (clipboard.active.ops && clipboard.active.ops->fileCancel)
    result = clipboard.active.ops->fileCancel(
        clipboard.active.opaque, dataset, request, reason);
  LG_UNLOCK_SHARED(clipboard.activeLock);
  return result;
}

void clipboard_fileRemoteReady(uint64_t dataset)
{
  LG_LOCK(clipboard.callbackLock);
  LG_LOCK(clipboard.stateLock);
  const bool notice = clipboard.localAvailable &&
    g_params.clipboardToLocal && clipboard.remoteNotice &&
    clipboard.remoteType == LG_CLIPBOARD_DATA_FILES &&
    clipboard.remoteFileDataset == dataset;
  LG_UNLOCK(clipboard.stateLock);
  if (notice && g_state.ds->cbNotice)
    g_state.ds->cbNotice(LG_CLIPBOARD_DATA_FILES);
  LG_UNLOCK(clipboard.callbackLock);
}

void clipboard_fileRemoteFailed(uint64_t dataset)
{
  if (!dataset)
    return;

  LG_LOCK(clipboard.callbackLock);
  LG_LOCK(clipboard.stateLock);
  const bool matchingDataset =
    clipboard.remoteFileDataset == dataset;
  const bool matchingNotice = matchingDataset && clipboard.remoteNotice &&
    clipboard.remoteType == LG_CLIPBOARD_DATA_FILES;
  const bool release = matchingNotice && clipboard.localAvailable &&
    g_params.clipboardToLocal;
  if (matchingDataset)
    clipboard.remoteFileDataset = 0;
  if (matchingNotice)
  {
    clipboard.remoteNotice     = false;
    clipboard.remoteType       = LG_CLIPBOARD_DATA_NONE;
    clipboard.remoteGeneration = nextGeneration(
        clipboard.remoteGeneration);
  }
  LG_UNLOCK(clipboard.stateLock);
  if (release && g_state.ds->cbRelease)
    g_state.ds->cbRelease();
  LG_UNLOCK(clipboard.callbackLock);
}
