/**
 * 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 <ntstatus.h>

#include "transport/lgmp/CLGMPClipboardTransport.h"

#include <wudfwdm.h>

#include "CDebug.h"
#include "Seq.h"
#include "WCCopy.h"
#include "WCWrite.h"
#include "transport/lgmp/CLGMPHost.h"

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

#ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION
  #define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x00000002
#endif

namespace
{
  static_assert(sizeof(LGMPStreamDescriptor) ==
      sizeof(KVMFRStreamDescriptor),
    "LGMP and KVMFR stream descriptor sizes differ");
  static_assert(offsetof(LGMPStreamDescriptor, offset) ==
      offsetof(KVMFRStreamDescriptor, offset),
    "LGMP and KVMFR stream descriptor offsets differ");
  static_assert(offsetof(LGMPStreamDescriptor, slotSize) ==
      offsetof(KVMFRStreamDescriptor, slotSize),
    "LGMP and KVMFR stream descriptor geometry differs");

  static const LGMPQueueConfig CLIPBOARD_QUEUE_CONFIG =
  {
    LGMP_Q_CLIPBOARD,
    LGMP_Q_CLIPBOARD_LEN,
    1000,
  };

  bool ArmPollTimer(HANDLE timer, uint32_t waitUs)
  {
    LARGE_INTEGER due = {};
    due.QuadPart = -static_cast<LONGLONG>(waitUs) * 10;
    return SetWaitableTimer(timer, &due, 0, nullptr, nullptr, FALSE) != FALSE;
  }

  bool EmptyControl(const KVMFRClipboardMessage& message,
    bool keepToken = false)
  {
    return !message.clipboardGeneration && !message.transfer &&
      !message.offset && !message.size && !message.format &&
      !message.flags && (keepToken || !message.token) &&
      !message.length && !message.sequence;
  }

  bool AddValid(uint64_t offset, uint32_t length)
  {
    return offset <= (std::numeric_limits<uint64_t>::max)() - length;
  }

  bool ValidOffer(const KVMFRClipboardMessage& message)
  {
    return message.clipboardGeneration && message.token &&
      !(message.token & ~KVMFR_CLIPBOARD_FORMAT_MASK_ALL) &&
      !message.transfer && !message.offset && !message.size &&
      !message.format && !message.flags && !message.length &&
      !message.sequence;
  }

  bool ValidClear(const KVMFRClipboardMessage& message)
  {
    return message.clipboardGeneration && !message.token &&
      !message.transfer && !message.offset && !message.size &&
      !message.format && !message.flags && !message.length &&
      !message.sequence;
  }

  bool ValidRequest(const KVMFRClipboardMessage& message, bool helper)
  {
    const bool validTransfer = helper ?
      kvmfrClipboardTransferFromHelper(message.transfer) :
      kvmfrClipboardTransferFromClient(message.transfer);
    return message.clipboardGeneration && validTransfer &&
      kvmfrClipboardRepresentationFormatValid(message.format) &&
      !message.offset && !message.size && !message.flags &&
      !message.token && !message.length && !message.sequence;
  }

  bool ValidCancel(const KVMFRClipboardMessage& message)
  {
    return message.transfer && !message.offset && !message.size &&
      (!message.format ||
        kvmfrClipboardRepresentationFormatValid(message.format)) &&
      !message.flags && !message.length && !message.sequence;
  }

  bool OwnerScopedLifecycle(const KVMFRClipboardMessage& message)
  {
    return message.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST ||
      message.type == KVMFR_CLIPBOARD_MESSAGE_DATA ||
      message.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL ||
      CLGMPClipboardFiles::IsRecord(message);
  }

  KVMFRStreamDescriptor StreamDescriptorToWire(
    const LGMPStreamDescriptor& descriptor)
  {
    KVMFRStreamDescriptor wire = {};
    wire.magic      = descriptor.magic;
    wire.version    = descriptor.version;
    wire.size       = descriptor.size;
    wire.offset     = descriptor.offset;
    wire.regionSize = descriptor.regionSize;
    wire.direction  = descriptor.direction;
    wire.policy     = descriptor.policy;
    wire.slotCount  = descriptor.slotCount;
    wire.slotSize   = descriptor.slotSize;
    return wire;
  }
}

CLGMPClipboardTransport::~CLGMPClipboardTransport()
{
  DeInit();
}

bool CLGMPClipboardTransport::Initialize()
{
  if (m_queue)
    return true;

  LGMP_STATUS status = m_host.CreateQueue(
    CLIPBOARD_QUEUE_CONFIG, &m_queue);
  if (status != LGMP_OK)
  {
    DEBUG_ERROR("lgmpHostQueueCreate Failed (Clipboard): %s",
      lgmpStatusString(status));
    return false;
  }

  for (PLGMPMemory& memory : m_statusMemory)
  {
    status = m_host.Allocate(sizeof(KVMFRClipboardStatus), &memory);
    if (status != LGMP_OK)
      goto fail;
    memset(lgmpHostMemPtr(memory), 0, sizeof(KVMFRClipboardStatus));
  }

  for (PLGMPMemory& memory : m_messageMemory)
  {
    status = m_host.Allocate(sizeof(KVMFRClipboardMessage), &memory);
    if (status != LGMP_OK)
      goto fail;
    memset(lgmpHostMemPtr(memory), 0, sizeof(KVMFRClipboardMessage));
  }

  if (!InitializeStreams())
  {
    DeInit();
    return false;
  }

  return true;

fail:
  DEBUG_ERROR("lgmpHostMemAlloc Failed (Clipboard): %s",
    lgmpStatusString(status));
  DeInit();
  return false;
}

void CLGMPClipboardTransport::DeInit()
{
  Stop();
  DeInitStreams();
  for (PLGMPMemory& memory : m_messageMemory)
    lgmpHostMemFree(&memory);
  for (PLGMPMemory& memory : m_statusMemory)
    lgmpHostMemFree(&memory);
  m_queue = nullptr;
}

bool CLGMPClipboardTransport::InitializeStreams()
{
  if (m_hostToClientStream && m_clientToHostStream)
    return true;

  const LGMPStreamConfig hostToClient =
  {
    LGMP_STREAM_HOST_TO_CLIENT,
    LGMP_STREAM_RELIABLE_FIFO,
    KVMFR_CLIPBOARD_STREAM_SLOT_COUNT,
    SLOT_BYTES,
  };
  LGMP_STATUS status = m_host.CreateStream(
    hostToClient, &m_hostToClientStream);
  if (status != LGMP_OK)
  {
    DEBUG_ERROR("Failed to create host-to-client clipboard stream: %s",
      lgmpStatusString(status));
    return false;
  }

  const LGMPStreamConfig clientToHost =
  {
    LGMP_STREAM_CLIENT_TO_HOST,
    LGMP_STREAM_RELIABLE_FIFO,
    KVMFR_CLIPBOARD_STREAM_SLOT_COUNT,
    SLOT_BYTES,
  };
  status = m_host.CreateStream(clientToHost, &m_clientToHostStream);
  if (status != LGMP_OK)
  {
    DEBUG_ERROR("Failed to create client-to-host clipboard stream: %s",
      lgmpStatusString(status));
    lgmpHostStreamFree(&m_hostToClientStream);
    return false;
  }

  LGMPStreamDescriptor descriptor = {};
  lgmpHostStreamGetDescriptor(m_hostToClientStream, &descriptor);
  m_hostToClientDescriptor = StreamDescriptorToWire(descriptor);
  lgmpHostStreamGetDescriptor(m_clientToHostStream, &descriptor);
  m_clientToHostDescriptor = StreamDescriptorToWire(descriptor);
  return true;
}

void CLGMPClipboardTransport::DeInitStreams()
{
  ForceUnbindStreams();
  lgmpHostStreamFree(&m_clientToHostStream);
  lgmpHostStreamFree(&m_hostToClientStream);
  m_clientToHostDescriptor = {};
  m_hostToClientDescriptor = {};
}

bool CLGMPClipboardTransport::BindStreams(uint32_t clientID) const
{
  if (!m_hostToClientStream || !m_clientToHostStream)
    return false;

  LGMP_STATUS status = lgmpHostStreamBind(
    m_hostToClientStream, clientID, nullptr);
  if (status != LGMP_OK)
  {
    DEBUG_ERROR("Failed to bind host-to-client clipboard stream for "
      "client %u: %s", clientID, lgmpStatusString(status));
    return false;
  }

  status = lgmpHostStreamBind(m_clientToHostStream, clientID, nullptr);
  if (status == LGMP_OK)
    return true;

  DEBUG_ERROR("Failed to bind client-to-host clipboard stream for "
    "client %u: %s", clientID, lgmpStatusString(status));
  const LGMP_STATUS rollback =
    lgmpHostStreamUnbind(m_hostToClientStream);
  if (rollback != LGMP_OK && rollback != LGMP_ERR_STREAM_UNBOUND &&
      rollback != LGMP_ERR_STREAM_BUSY)
    DEBUG_ERROR("Failed to roll back host-to-client clipboard stream "
      "binding: %s", lgmpStatusString(rollback));
  return false;
}

bool CLGMPClipboardTransport::TryUnbindStreams()
{
  bool complete = true;
  if (m_clientToHostStream)
  {
    const LGMP_STATUS status =
      lgmpHostStreamUnbind(m_clientToHostStream);
    if (status == LGMP_ERR_STREAM_BUSY)
      complete = false;
    else if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND)
    {
      Fail("lgmpHostStreamUnbind(client-to-host)", status);
      complete = false;
    }
  }
  if (m_hostToClientStream)
  {
    const LGMP_STATUS status =
      lgmpHostStreamUnbind(m_hostToClientStream);
    if (status == LGMP_ERR_STREAM_BUSY)
      complete = false;
    else if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND)
    {
      Fail("lgmpHostStreamUnbind(host-to-client)", status);
      complete = false;
    }
  }
  return complete;
}

void CLGMPClipboardTransport::ForceUnbindStreams() const
{
  if (m_clientToHostStream)
  {
    const LGMP_STATUS status =
      lgmpHostStreamForceUnbind(m_clientToHostStream);
    if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND)
      DEBUG_ERROR("Failed to force-unbind client-to-host clipboard "
        "stream: %s", lgmpStatusString(status));
  }
  if (m_hostToClientStream)
  {
    const LGMP_STATUS status =
      lgmpHostStreamForceUnbind(m_hostToClientStream);
    if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND)
      DEBUG_ERROR("Failed to force-unbind host-to-client clipboard "
        "stream: %s", lgmpStatusString(status));
  }
}

void CLGMPClipboardTransport::Wake() const
{
  if (m_wakeEvent)
    SetEvent(m_wakeEvent);
}

bool CLGMPClipboardTransport::Start(IClipboardTarget& target)
{
  CSRWExclusiveLock lifecycleLock(m_lifecycleLock);
  if (m_thread)
  {
    if (WaitForSingleObject(m_thread, 0) == WAIT_TIMEOUT)
      return true;

    CloseHandle(m_thread);
    CloseHandle(m_pollTimer);
    CloseHandle(m_wakeEvent);
    CloseHandle(m_stopEvent);
    m_thread    = nullptr;
    m_pollTimer = nullptr;
    m_wakeEvent = nullptr;
    m_stopEvent = nullptr;
  }
  if (!m_queue)
    return false;

  m_stopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
  if (!m_stopEvent)
  {
    const DWORD error = GetLastError();
    DEBUG_ERROR_HR(error,
      "Failed to create LGMP clipboard stop event");
    m_stopEvent = nullptr;
    return false;
  }

  m_wakeEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr);
  if (!m_wakeEvent)
  {
    const DWORD error = GetLastError();
    DEBUG_ERROR_HR(error,
      "Failed to create LGMP clipboard wake event");
    CloseHandle(m_stopEvent);
    m_stopEvent = nullptr;
    return false;
  }

  m_pollTimer = CreateWaitableTimerExW(nullptr, nullptr,
    CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
  if (!m_pollTimer)
    m_pollTimer = CreateWaitableTimerExW(
      nullptr, nullptr, 0, TIMER_ALL_ACCESS);
  if (!m_pollTimer)
  {
    const DWORD error = GetLastError();
    DEBUG_ERROR_HR(error,
      "Failed to create LGMP clipboard poll timer");
    CloseHandle(m_wakeEvent);
    CloseHandle(m_stopEvent);
    m_wakeEvent = nullptr;
    m_stopEvent = nullptr;
    return false;
  }

  {
    CSRWExclusiveLock lock(m_lock);
    m_target = &target;
    m_failed = false;
    m_statusDirty = true;
  }
  m_thread = CreateThread(nullptr, 0, ThreadProc, this, 0, nullptr);
  if (!m_thread)
  {
    DEBUG_ERROR_HR(GetLastError(),
      "Failed to create LGMP clipboard worker");
    {
      CSRWExclusiveLock lock(m_lock);
      m_target = nullptr;
    }
    CloseHandle(m_pollTimer);
    CloseHandle(m_wakeEvent);
    CloseHandle(m_stopEvent);
    m_pollTimer = nullptr;
    m_wakeEvent = nullptr;
    m_stopEvent = nullptr;
    return false;
  }
  return true;
}

void CLGMPClipboardTransport::Stop()
{
  CSRWExclusiveLock lifecycleLock(m_lifecycleLock);
  if (m_stopEvent)
    SetEvent(m_stopEvent);
  if (m_thread)
    WaitForSingleObject(m_thread, INFINITE);

  if (m_thread)
    CloseHandle(m_thread);
  if (m_pollTimer)
    CloseHandle(m_pollTimer);
  if (m_wakeEvent)
    CloseHandle(m_wakeEvent);
  if (m_stopEvent)
    CloseHandle(m_stopEvent);
  m_thread    = nullptr;
  m_pollTimer = nullptr;
  m_wakeEvent = nullptr;
  m_stopEvent = nullptr;

  CSRWExclusiveLock lock(m_lock);
  DropPendingTarget();
  ResetProtocol(false);
  m_available = false;
  m_endpointGeneration = 0;
  m_target = nullptr;
  m_failed = false;
  m_statusDirty = true;
}

bool CLGMPClipboardTransport::IsOwner(
  uint32_t clientID, uint32_t generation) const
{
  return m_ownerClientID == clientID &&
    m_ownerGeneration == generation;
}

bool CLGMPClipboardTransport::OwnerSubscribed() const
{
  if (!m_ownerClientID)
    return false;

  uint32_t clients[LGMP_MAX_CLIENTS] = {};
  unsigned count = 0;
  if (lgmpHostGetClientIDs(m_queue, clients, &count) != LGMP_OK)
    return false;
  for (unsigned i = 0; i < count; ++i)
    if (clients[i] == m_ownerClientID)
      return true;
  return false;
}

void CLGMPClipboardTransport::RenewLease()
{
  m_ownerDeadline = GetTickCount64() + OWNER_LEASE_MS;
}

void CLGMPClipboardTransport::BlockOutbound(
  const KVMFRClipboardMessage& record, uint32_t ownerClientID,
  uint32_t ownerGeneration)
{
  m_outboundBlocked = true;
  m_blockedOutbound = record;
  m_blockedOwnerClientID = ownerClientID;
  m_blockedOwnerGeneration = ownerGeneration;
}

void CLGMPClipboardTransport::ClearOutboundBlock()
{
  m_outboundBlocked = false;
  m_blockedOutbound = {};
  m_blockedOwnerClientID = 0;
  m_blockedOwnerGeneration = 0;
}

bool CLGMPClipboardTransport::BlockedOwnerLost(
  const KVMFRClipboardMessage& record) const
{
  return m_outboundBlocked && OwnerScopedLifecycle(record) &&
    memcmp(&m_blockedOutbound, &record, sizeof(record)) == 0 &&
    (!m_available || m_ownerReleasing ||
     record.generation != m_endpointGeneration ||
     m_blockedOwnerClientID != m_ownerClientID ||
     m_blockedOwnerGeneration != m_ownerGeneration);
}

void CLGMPClipboardTransport::QueueHelperClear()
{
  if (!m_clientClipboardGeneration || !m_target ||
      !m_available || !m_endpointGeneration)
    return;

  KVMFRClipboardMessage clear = {};
  clear.version             = KVMFR_CLIPBOARD_VERSION;
  clear.type                = KVMFR_CLIPBOARD_MESSAGE_CLEAR;
  clear.generation          = m_endpointGeneration;
  clear.clipboardGeneration = m_clientClipboardGeneration;
  m_clientClipboardGeneration = 0;
  m_clientFormats = 0;
  QueueInternalTarget(clear);
}

void CLGMPClipboardTransport::QueueTransferCancel(
  const Transfer& transfer)
{
  if (!transfer.Active() || !m_target || !m_available ||
      !m_endpointGeneration)
    return;

  KVMFRClipboardMessage cancel = {};
  cancel.version             = KVMFR_CLIPBOARD_VERSION;
  cancel.type                = KVMFR_CLIPBOARD_MESSAGE_CANCEL;
  cancel.generation          = m_endpointGeneration;
  cancel.clipboardGeneration = transfer.clipboardGeneration;
  cancel.transfer            = transfer.transfer;
  cancel.format              = transfer.format;
  cancel.token               = ERROR_DEVICE_NOT_CONNECTED;
  QueueInternalTarget(cancel);
}

void CLGMPClipboardTransport::QueueStaleRequestCancel(
  const KVMFRClipboardMessage& request)
{
  if (request.type != KVMFR_CLIPBOARD_MESSAGE_REQUEST ||
      !m_target || !m_available || !m_endpointGeneration)
    return;

  KVMFRClipboardMessage cancel = {};
  cancel.version             = KVMFR_CLIPBOARD_VERSION;
  cancel.type                = KVMFR_CLIPBOARD_MESSAGE_CANCEL;
  cancel.generation          = m_endpointGeneration;
  cancel.clipboardGeneration = request.clipboardGeneration;
  cancel.transfer            = request.transfer;
  cancel.format              = request.format;
  cancel.token               = ERROR_DEVICE_NOT_CONNECTED;
  QueueInternalTarget(cancel);
}

void CLGMPClipboardTransport::QueueStaleFileRecord(
  const KVMFRClipboardMessage& record)
{
  if (!m_target || !m_available || !m_endpointGeneration)
    return;

  KVMFRClipboardMessage response = {};
  response.version             = KVMFR_CLIPBOARD_VERSION;
  response.generation          = m_endpointGeneration;
  response.clipboardGeneration = record.clipboardGeneration;
  response.transfer            = record.transfer;
  response.format              = KVMFR_CLIPBOARD_FORMAT_FILES;
  response.token               = KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED;
  if (record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE)
    response.type = KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED;
  else if (record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST)
    response.type = KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL;
  else
    return;
  QueueInternalTarget(response);
}

void CLGMPClipboardTransport::QueueFileDisconnect()
{
  KVMFRClipboardMessage records[
    CLGMPClipboardFiles::MAX_ACQUISITIONS +
    CLGMPClipboardFiles::MAX_REQUESTS] = {};
  const unsigned count = m_files.Disconnect(records, _countof(records),
    m_endpointGeneration);
  for (unsigned i = 0; i < count; ++i)
    QueueInternalTarget(records[i]);
}

bool CLGMPClipboardTransport::QueueInternalTarget(
  const KVMFRClipboardMessage& record)
{
  if (!m_pendingTarget.valid && !m_streamTargetPrepared &&
      !m_internalTargetCount)
    return BeginTarget(record, false);
  if (m_internalTargetCount == INTERNAL_TARGET_COUNT)
  {
    m_failed = true;
    return false;
  }
  m_internalTarget[m_internalTargetCount++] = record;
  return true;
}

bool CLGMPClipboardTransport::PumpInternalTarget()
{
  if (m_pendingTarget.valid || m_streamTargetPrepared ||
      !m_internalTargetCount)
    return true;

  const KVMFRClipboardMessage record = m_internalTarget[0];
  for (unsigned i = 1; i < m_internalTargetCount; ++i)
    m_internalTarget[i - 1] = m_internalTarget[i];
  m_internalTarget[--m_internalTargetCount] = {};
  return BeginTarget(record, false);
}

void CLGMPClipboardTransport::ClearStreamTargets()
{
  for (StreamTarget& target : m_streamTarget)
    target.Clear();
  m_streamTargetHead     = 0;
  m_streamTargetCount    = 0;
  m_streamTargetPrepared = 0;
}

void CLGMPClipboardTransport::ClearQueuedStreamTargets()
{
  if (!m_streamTargetPrepared)
  {
    ClearStreamTargets();
    return;
  }

  // The prepared prefix has already crossed the target boundary and must
  // receive the same drain treatment as a BUSY PendingTarget. Only discard
  // records which have not yet been presented to the target.
  for (unsigned i = m_streamTargetPrepared;
      i < m_streamTargetCount; ++i)
    m_streamTarget[
      (m_streamTargetHead + i) % STREAM_TARGET_COUNT].Clear();
  m_streamTargetCount = m_streamTargetPrepared;
}

bool CLGMPClipboardTransport::QueueStreamTarget(
  const KVMFRClipboardMessage& record, const uint8_t * data)
{
  if (m_streamTargetCount == STREAM_TARGET_COUNT ||
      (record.length && !data))
    return false;

  StreamTarget& target = m_streamTarget[
    (m_streamTargetHead + m_streamTargetCount) % STREAM_TARGET_COUNT];
  target.record = record;
  if (record.length)
    WCCopy::Copy(target.data, data, record.length);
  ++m_streamTargetCount;
  return true;
}

void CLGMPClipboardTransport::PopStreamTargets(unsigned count)
{
  for (unsigned i = 0; i < count; ++i)
    m_streamTarget[
      (m_streamTargetHead + i) % STREAM_TARGET_COUNT].Clear();
  m_streamTargetHead =
    (m_streamTargetHead + count) % STREAM_TARGET_COUNT;
  m_streamTargetCount -= count;
  m_streamTargetPrepared = count < m_streamTargetPrepared ?
    m_streamTargetPrepared - count : 0;
}

CLGMPClipboardTransport::StreamDisposition
CLGMPClipboardTransport::PreviewStreamTarget(
  const KVMFRClipboardMessage& record, Transfer& transfer,
  CLGMPClipboardFiles& files)
{
  if (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA)
  {
    if (record.transfer == m_discardClientToHelper)
      return StreamDisposition::DISCARD;
    if (record.token ||
        !kvmfrClipboardTransferFromHelper(record.transfer) ||
        !ValidateChunk(transfer, record))
      return StreamDisposition::INVALID;
    AdvanceChunk(transfer, record);
    return StreamDisposition::READY;
  }

  const CLGMPClipboardFiles::Direction direction =
    CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER;
  if (files.IsStaleTerminal(record, direction))
    return StreamDisposition::STALE;
  if (!files.Validate(record, direction, 0, false))
    return StreamDisposition::INVALID;
  files.Apply(record, direction);
  return StreamDisposition::READY;
}

bool CLGMPClipboardTransport::PublishStreamTargets()
{
  ClipboardChannelWrite writes[STREAM_TARGET_COUNT] = {};
  for (unsigned i = 0; i < m_streamTargetPrepared; ++i)
  {
    StreamTarget& target = m_streamTarget[
      (m_streamTargetHead + i) % STREAM_TARGET_COUNT];
    writes[i].record = target.record;
    writes[i].data   = target.record.length ? target.data : nullptr;
  }

  size_t accepted = 0;
  const ClipboardChannelResult result = m_target ?
    m_target->SendClipboardBatch(writes, m_streamTargetPrepared,
      accepted) :
    ClipboardChannelResult::FAILED;
  if (accepted > m_streamTargetPrepared ||
      (result == ClipboardChannelResult::ACCEPTED &&
        accepted != m_streamTargetPrepared))
  {
    DEBUG_ERROR("Helper accepted an invalid clipboard stream prefix: "
      "accepted=%zu offered=%u", accepted, m_streamTargetPrepared);
    ReleaseOwner("invalid Helper clipboard batch result", false);
    m_failed = true;
    return true;
  }

  for (size_t i = 0; i < accepted; ++i)
  {
    const StreamTarget& target = m_streamTarget[
      (m_streamTargetHead + i) % STREAM_TARGET_COUNT];
    ApplyInbound(target.record);
  }
  if (accepted)
  {
    PopStreamTargets(static_cast<unsigned>(accepted));
    if (m_ownerClientID && !m_ownerReleasing)
      RenewLease();
  }

  if (result == ClipboardChannelResult::BUSY)
  {
    if (m_ownerClientID)
      RenewLease();
    return true;
  }

  if (result == ClipboardChannelResult::FAILED)
  {
    ReleaseOwner("Helper delivery failed", false);
    m_failed = true;
  }
  return true;
}

bool CLGMPClipboardTransport::RetryStreamTargets()
{
  if (!m_streamTargetPrepared || m_pendingTarget.valid)
    return true;

  // The retained prefix is already validated and stamped. Retry it exactly;
  // owner-release state may have changed while the target was BUSY.
  return PublishStreamTargets();
}

bool CLGMPClipboardTransport::PumpStreamTarget()
{
  while (!m_pendingTarget.valid && !m_streamTargetPrepared &&
      m_streamTargetCount)
  {
    if (!m_ownerClientID ||
        (m_ownerReleasing && !m_releaseClearHelper))
    {
      PopStreamTargets(1);
      continue;
    }

    // Dependent chunks are validated against the state the preceding chunk
    // would establish, without committing that state before Helper accepts
    // the corresponding prefix.
    Transfer transfer = m_clientToHelper;
    CLGMPClipboardFiles files = m_files;
    StreamDisposition disposition = StreamDisposition::READY;
    for (unsigned i = 0; i < m_streamTargetCount; ++i)
    {
      StreamTarget& target = m_streamTarget[
        (m_streamTargetHead + i) % STREAM_TARGET_COUNT];
      disposition = PreviewStreamTarget(target.record, transfer, files);
      if (disposition != StreamDisposition::READY)
        break;
      target.record.generation = m_endpointGeneration;
      ++m_streamTargetPrepared;
    }

    if (m_streamTargetPrepared)
    {
      if (!m_ownerReleasing)
        RenewLease();
      if (!PublishStreamTargets())
        return false;
      if (m_streamTargetPrepared || m_failed)
        return true;
      continue;
    }

    PopStreamTargets(1);
    if (disposition == StreamDisposition::STALE)
    {
      if (!m_ownerReleasing)
        RenewLease();
      continue;
    }
    if (disposition == StreamDisposition::INVALID)
      ReleaseOwner("invalid clipboard stream data", true);
  }
  return true;
}

bool CLGMPClipboardTransport::DrainStream(bool& received)
{
  received = false;
  if (m_failed || !m_ownerClientID || !m_clientToHostStream)
    return true;

  for (unsigned drained = 0; drained < STREAM_DRAIN_MAX;)
  {
    if (m_streamTargetCount == STREAM_TARGET_COUNT)
    {
      // Submit only after the bounded staging window is full. A retained
      // prefix or pending control keeps the window backpressured here.
      if (!PumpStreamTarget())
        return false;
      if (m_streamTargetCount == STREAM_TARGET_COUNT)
        return true;
    }

    LGMPStreamBuffer buffer = {};
    LGMP_STATUS status =
      lgmpHostStreamReadPeek(m_clientToHostStream, &buffer);
    if (status == LGMP_ERR_STREAM_EMPTY)
      break;
    if (status == LGMP_ERR_STREAM_UNBOUND ||
        status == LGMP_ERR_STREAM_STALE)
    {
      if (m_ownerReleasing && status == LGMP_ERR_STREAM_UNBOUND)
        break;
      if (!PumpStreamTarget())
        return false;
      ReleaseOwner("clipboard stream binding lost", true);
      return true;
    }
    if (status != LGMP_OK)
    {
      if (!PumpStreamTarget())
        return false;
      Fail("lgmpHostStreamReadPeek", status);
      return false;
    }
    received = true;

    KVMFRClipboardMessage record = {};
    if (buffer.data && buffer.size >= sizeof(record))
      memcpy(&record, buffer.data, sizeof(record));
    const bool valid = buffer.data &&
      buffer.size == sizeof(record) + record.length &&
      record.version == KVMFR_CLIPBOARD_VERSION &&
      record.generation == m_ownerGeneration &&
      (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA ||
       record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_DATA) &&
      record.length <= KVMFR_CLIPBOARD_DATA_BYTES &&
      !(record.flags & ~(KVMFR_CLIPBOARD_FLAG_BEGIN |
        KVMFR_CLIPBOARD_FLAG_END)) &&
      AddValid(record.offset, record.length);
    const bool queued = valid && QueueStreamTarget(record,
      static_cast<const uint8_t *>(buffer.data) + sizeof(record));

    status = lgmpHostStreamReadRelease(m_clientToHostStream, &buffer);
    if (status != LGMP_OK)
    {
      Fail("lgmpHostStreamReadRelease", status);
      return false;
    }
    if (!queued)
    {
      if (!PumpStreamTarget())
        return false;
      ReleaseOwner("invalid clipboard stream record", true);
      return true;
    }
    ++drained;
  }

  return PumpStreamTarget();
}

void CLGMPClipboardTransport::ReleaseOwner(
  const char * reason, bool clearHelper, bool force)
{
  if (!m_ownerClientID)
    return;

  if (!m_ownerReleasing)
  {
    m_ownerReleasing      = true;
    m_releaseClearHelper  = clearHelper;
    m_ownerReleaseReason  = reason;
    m_ownerDeadline       = 0;
    m_ownerDrainDeadline  = GetTickCount64() + STREAM_DRAIN_TIMEOUT_MS;
    m_replayPending       = false;
    ClearOutboundBlock();
  }
  else if (force)
  {
    m_releaseClearHelper |= clearHelper;
    m_ownerReleaseReason = reason;
  }

  if (force)
  {
    ForceUnbindStreams();
    FinishOwnerRelease();
    return;
  }

  if (TryUnbindStreams() && !m_pendingTarget.valid &&
      !m_streamTargetCount)
    FinishOwnerRelease();
}

void CLGMPClipboardTransport::FinishOwnerRelease()
{
  if (!m_ownerClientID)
    return;

  const uint32_t clientID     = m_ownerClientID;
  const uint32_t generation   = m_ownerGeneration;
  const char * const reason   = m_ownerReleaseReason ?
    m_ownerReleaseReason : "unspecified";
  const Transfer clientToHelper = m_clientToHelper;
  const Transfer helperToClient = m_helperToClient;
  const bool clearHelper      = m_releaseClearHelper;
  m_ownerClientID             = 0;
  m_ownerGeneration           = 0;
  m_ownerReleasing            = false;
  m_releaseClearHelper        = false;
  m_ownerReleaseReason        = nullptr;
  m_ownerDeadline             = 0;
  m_ownerDrainDeadline        = 0;
  m_replayPending             = false;
  ClearQueuedStreamTargets();
  m_discardClientToHelper     = 0;
  m_clientToHelper.Clear();
  m_helperToClient.Clear();
  if (helperToClient.Active())
    m_discardHelperToClient = helperToClient.transfer;
  m_statusDirty               = true;

  if (clearHelper)
  {
    QueueFileDisconnect();
    QueueTransferCancel(clientToHelper);
    QueueTransferCancel(helperToClient);
    QueueHelperClear();
  }
  else
  {
    m_files.Reset();
    m_clientClipboardGeneration = 0;
    m_clientFormats = 0;
  }

  DEBUG_INFO("Clipboard owner %u generation %u released (%s)",
    clientID, generation, reason);
}

bool CLGMPClipboardTransport::RetryOwnerRelease()
{
  if (!m_ownerReleasing)
    return true;

  if (GetTickCount64() >= m_ownerDrainDeadline)
  {
    DEBUG_WARN("Timed out draining clipboard streams for owner %u "
      "generation %u; discarding in-flight records",
      m_ownerClientID, m_ownerGeneration);
    DropPendingTarget();
    ClearStreamTargets();
    ForceUnbindStreams();
    FinishOwnerRelease();
    return true;
  }

  if (TryUnbindStreams() && !m_pendingTarget.valid &&
      !m_streamTargetCount)
    FinishOwnerRelease();
  return !m_failed;
}

void CLGMPClipboardTransport::ResetProtocol(bool keepClipboard)
{
  DropPendingTarget();
  if (m_ownerClientID)
    ReleaseOwner("endpoint reset", false);
  m_clientToHelper.Clear();
  m_helperToClient.Clear();
  m_files.Reset();
  m_clientClipboardGeneration = 0;
  m_clientFormats = 0;
  m_discardClientToHelper = 0;
  m_discardHelperToClient = 0;
  m_replayPending = false;
  ClearOutboundBlock();
  m_internalTargetCount = 0;
  ClearStreamTargets();
  for (KVMFRClipboardMessage& record : m_internalTarget)
    record = {};
  if (!keepClipboard)
  {
    m_cachedValid = false;
    m_cachedClipboard = {};
    m_helperFormats = 0;
  }
  m_statusDirty = true;
}

PLGMPMemory CLGMPClipboardTransport::FindAvailable(
  PLGMPMemory (&memory)[MEMORY_COUNT]) const
{
  for (PLGMPMemory candidate : memory)
    if (candidate && !lgmpHostQueuePayloadPending(m_queue, candidate))
      return candidate;
  return nullptr;
}

CLGMPClipboardTransport::PostResult
CLGMPClipboardTransport::PostForOwner(
  uint64_t udata, PLGMPMemory memory)
{
  if (!m_ownerClientID)
    return PostResult::GONE;

  unsigned recipients = 0;
  const uint32_t clientID = m_ownerClientID;
  const LGMP_STATUS status = lgmpHostQueuePostForClients(
    m_queue, udata, memory, &clientID, 1, &recipients);
  if (status == LGMP_ERR_QUEUE_FULL)
    return PostResult::BUSY;
  if (status != LGMP_OK)
  {
    Fail("lgmpHostQueuePostForClients", status);
    return PostResult::FAILED;
  }
  return recipients ? PostResult::POSTED : PostResult::GONE;
}

void CLGMPClipboardTransport::Fail(
  const char * operation, LGMP_STATUS status)
{
  if (!m_failed)
    DEBUG_ERROR("%s Failed (Clipboard): %s",
      operation, lgmpStatusString(status));
  m_failed = true;
  if (m_stopEvent)
    SetEvent(m_stopEvent);
}

bool CLGMPClipboardTransport::PublishStatus()
{
  if (lgmpHostQueueNewSubs(m_queue))
    m_statusDirty = true;
  if (!m_statusDirty)
    return true;

  uint32_t clients[LGMP_MAX_CLIENTS] = {};
  unsigned clientCount = 0;
  LGMP_STATUS status =
    lgmpHostGetClientIDs(m_queue, clients, &clientCount);
  if (status != LGMP_OK)
  {
    Fail("lgmpHostGetClientIDs", status);
    return false;
  }
  if (!clientCount)
    return true;

  PLGMPMemory memory = FindAvailable(m_statusMemory);
  if (!memory)
    return true;

  KVMFRClipboardStatus clipboardStatus = {};
  clipboardStatus.version         = KVMFR_CLIPBOARD_VERSION;
  clipboardStatus.generation      = m_endpointGeneration;
  clipboardStatus.lease           = static_cast<uint32_t>(OWNER_LEASE_MS);
  clipboardStatus.slotBytes       = KVMFR_CLIPBOARD_DATA_BYTES;
  clipboardStatus.streamVersion   = KVMFR_CLIPBOARD_STREAM_VERSION;
  clipboardStatus.streamSlotCount = KVMFR_CLIPBOARD_STREAM_SLOT_COUNT;
  clipboardStatus.hostToClient    = m_hostToClientDescriptor;
  clipboardStatus.clientToHost    = m_clientToHostDescriptor;
  if (m_available)
  {
    clipboardStatus.flags |= KVMFR_CLIPBOARD_STATUS_AVAILABLE;
    clipboardStatus.formats = m_helperFormats;
  }
  if (m_available && m_ownerClientID)
  {
    clipboardStatus.flags |= KVMFR_CLIPBOARD_STATUS_HAS_OWNER;
    clipboardStatus.ownerClientID   = m_ownerClientID;
    clipboardStatus.ownerGeneration = m_ownerGeneration;
  }
  memcpy(lgmpHostMemPtr(memory), &clipboardStatus,
    sizeof(clipboardStatus));

  const uint32_t serial = Seq::Next(m_statusSerial);
  unsigned recipients = 0;
  status = lgmpHostQueuePostForClients(m_queue,
    KVMFR_CLIPBOARD_QUEUE_UDATA(KVMFR_CLIPBOARD_QUEUE_STATUS, serial),
    memory, clients, clientCount, &recipients);
  if (status == LGMP_ERR_QUEUE_FULL)
    return true;
  if (status != LGMP_OK)
  {
    Fail("lgmpHostQueuePostForClients", status);
    return false;
  }
  if (recipients)
  {
    m_statusSerial = serial;
    m_statusDirty = false;
  }
  return true;
}

bool CLGMPClipboardTransport::ReplayClipboard()
{
  if (!m_replayPending || !m_ownerClientID || m_ownerReleasing)
    return true;
  if (!m_cachedValid)
  {
    m_replayPending = false;
    return true;
  }

  PLGMPMemory memory = FindAvailable(m_messageMemory);
  if (!memory)
    return true;
  KVMFRClipboardMessage message = m_cachedClipboard;
  message.generation = m_ownerGeneration;
  memcpy(lgmpHostMemPtr(memory), &message, sizeof(message));

  const uint32_t serial = Seq::Next(m_messageSerial);
  const PostResult result = PostForOwner(
    KVMFR_CLIPBOARD_QUEUE_UDATA(
      KVMFR_CLIPBOARD_QUEUE_MESSAGE, serial), memory);
  if (result == PostResult::POSTED)
  {
    m_messageSerial = serial;
    m_replayPending = false;
  }
  else if (result == PostResult::GONE)
    ReleaseOwner("subscriber disappeared", true, true);
  return result != PostResult::FAILED;
}

bool CLGMPClipboardTransport::ValidateClaim(
  const KVMFRClipboardMessage& message) const
{
  return message.version == KVMFR_CLIPBOARD_VERSION &&
    message.type == KVMFR_CLIPBOARD_MESSAGE_CLAIM &&
    message.generation && message.token == m_endpointGeneration &&
    m_hostToClientStream && m_clientToHostStream &&
    EmptyControl(message, true);
}

bool CLGMPClipboardTransport::ValidateOwnedControl(
  const KVMFRClipboardMessage& message) const
{
  if (message.version != KVMFR_CLIPBOARD_VERSION ||
      message.generation != m_ownerGeneration)
    return false;

  switch (message.type)
  {
    case KVMFR_CLIPBOARD_MESSAGE_RELEASE:
    case KVMFR_CLIPBOARD_MESSAGE_KEEPALIVE:
      return EmptyControl(message);

    case KVMFR_CLIPBOARD_MESSAGE_OFFER:
    case KVMFR_CLIPBOARD_MESSAGE_CLEAR:
    case KVMFR_CLIPBOARD_MESSAGE_REQUEST:
    case KVMFR_CLIPBOARD_MESSAGE_CANCEL:
      return ValidateInboundRecord(message);

    case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE:
    case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED:
    case KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE:
    case KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST:
    case KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL:
      return ValidateInboundRecord(message);

    default:
      return false;
  }
}

bool CLGMPClipboardTransport::ValidateInboundRecord(
  const KVMFRClipboardMessage& message) const
{
  if (CLGMPClipboardFiles::IsRecord(message))
  {
    if (m_files.IsStaleTerminal(message,
        CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER))
      return true;
    const bool filesOffered = m_cachedValid &&
      m_cachedClipboard.type == KVMFR_CLIPBOARD_MESSAGE_OFFER &&
      (m_helperFormats & KVMFR_CLIPBOARD_FORMAT_MASK_FILES);
    const uint64_t offeredDataset = filesOffered ?
      m_cachedClipboard.clipboardGeneration : 0;
    return m_files.Validate(message,
      CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER,
      offeredDataset, filesOffered);
  }

  switch (message.type)
  {
    case KVMFR_CLIPBOARD_MESSAGE_OFFER:
      return ValidOffer(message);

    case KVMFR_CLIPBOARD_MESSAGE_CLEAR:
      return ValidClear(message);

    case KVMFR_CLIPBOARD_MESSAGE_REQUEST:
      return ValidRequest(message, false);

    case KVMFR_CLIPBOARD_MESSAGE_CANCEL:
      return ValidCancel(message);

    default:
      return false;
  }
}

bool CLGMPClipboardTransport::ValidateOutboundRecord(
  const KVMFRClipboardMessage& message) const
{
  if (message.version != KVMFR_CLIPBOARD_VERSION ||
      message.generation != m_endpointGeneration)
    return false;

  if (CLGMPClipboardFiles::IsRecord(message))
  {
    const bool filesOffered = m_clientClipboardGeneration &&
      (m_clientFormats & KVMFR_CLIPBOARD_FORMAT_MASK_FILES);
    return m_files.Validate(message,
      CLGMPClipboardFiles::Direction::HELPER_TO_CLIENT,
      m_clientClipboardGeneration, filesOffered);
  }

  switch (message.type)
  {
    case KVMFR_CLIPBOARD_MESSAGE_OFFER:
      return ValidOffer(message);

    case KVMFR_CLIPBOARD_MESSAGE_CLEAR:
      return ValidClear(message);

    case KVMFR_CLIPBOARD_MESSAGE_REQUEST:
      return ValidRequest(message, true);

    case KVMFR_CLIPBOARD_MESSAGE_DATA:
      return message.clipboardGeneration &&
        kvmfrClipboardTransferFromClient(message.transfer) &&
        kvmfrClipboardRepresentationFormatValid(message.format) &&
        message.length <= KVMFR_CLIPBOARD_DATA_BYTES &&
        !(message.flags & ~(KVMFR_CLIPBOARD_FLAG_BEGIN |
          KVMFR_CLIPBOARD_FLAG_END)) && !message.token &&
        AddValid(message.offset, message.length);

    case KVMFR_CLIPBOARD_MESSAGE_CANCEL:
      return ValidCancel(message);

    default:
      return false;
  }
}

bool CLGMPClipboardTransport::ValidateChunk(
  const Transfer& transfer, const KVMFRClipboardMessage& message) const
{
  if (!transfer.Active() || message.transfer != transfer.transfer ||
      message.clipboardGeneration != transfer.clipboardGeneration ||
      message.format != transfer.format ||
      message.offset != transfer.nextOffset ||
      message.sequence != transfer.nextSequence ||
      !AddValid(message.offset, message.length))
    return false;

  const uint64_t end = message.offset + message.length;
  if (!message.length &&
      !(message.flags & (KVMFR_CLIPBOARD_FLAG_BEGIN |
        KVMFR_CLIPBOARD_FLAG_END)))
    return false;
  if (!transfer.began)
  {
    if (message.offset || message.sequence ||
        !(message.flags & KVMFR_CLIPBOARD_FLAG_BEGIN))
      return false;
  }
  else if (message.flags & KVMFR_CLIPBOARD_FLAG_BEGIN)
    return false;
  else if (!(message.flags & KVMFR_CLIPBOARD_FLAG_END) &&
      message.size != KVMFR_CLIPBOARD_SIZE_UNKNOWN)
    return false;

  const uint64_t hint = transfer.began ? transfer.sizeHint : message.size;
  if (hint != KVMFR_CLIPBOARD_SIZE_UNKNOWN && end > hint)
    return false;
  if (message.flags & KVMFR_CLIPBOARD_FLAG_END)
    return message.size == end &&
      (hint == KVMFR_CLIPBOARD_SIZE_UNKNOWN || hint == end);
  return true;
}

void CLGMPClipboardTransport::AdvanceChunk(
  Transfer& transfer, const KVMFRClipboardMessage& message)
{
  if (!transfer.began)
  {
    transfer.began = true;
    transfer.sizeHint = message.size;
  }
  transfer.nextOffset += message.length;
  ++transfer.nextSequence;
  if (message.flags & KVMFR_CLIPBOARD_FLAG_END)
    transfer.Clear();
}

void CLGMPClipboardTransport::ApplyInbound(
  const KVMFRClipboardMessage& message)
{
  if (CLGMPClipboardFiles::IsRecord(message))
  {
    m_files.Apply(message,
      CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER);
    return;
  }

  switch (message.type)
  {
    case KVMFR_CLIPBOARD_MESSAGE_OFFER:
      m_clientClipboardGeneration = message.clipboardGeneration;
      m_clientFormats = message.token;
      if (m_clientToHelper.Active())
        m_discardClientToHelper = m_clientToHelper.transfer;
      m_clientToHelper.Clear();
      break;

    case KVMFR_CLIPBOARD_MESSAGE_CLEAR:
      m_clientClipboardGeneration = 0;
      m_clientFormats = 0;
      if (m_clientToHelper.Active())
        m_discardClientToHelper = m_clientToHelper.transfer;
      m_clientToHelper.Clear();
      break;

    case KVMFR_CLIPBOARD_MESSAGE_REQUEST:
      m_helperToClient.Clear();
      m_helperToClient.transfer = message.transfer;
      m_helperToClient.clipboardGeneration =
        message.clipboardGeneration;
      m_helperToClient.format = message.format;
      break;

    case KVMFR_CLIPBOARD_MESSAGE_DATA:
      AdvanceChunk(m_clientToHelper, message);
      break;

    case KVMFR_CLIPBOARD_MESSAGE_CANCEL:
      if (m_clientToHelper.transfer == message.transfer)
      {
        m_discardClientToHelper = message.transfer;
        m_clientToHelper.Clear();
      }
      if (m_helperToClient.transfer == message.transfer)
        m_helperToClient.Clear();
      break;
  }
}

void CLGMPClipboardTransport::ApplyOutbound(
  const KVMFRClipboardMessage& message)
{
  if (CLGMPClipboardFiles::IsRecord(message))
  {
    m_files.Apply(message,
      CLGMPClipboardFiles::Direction::HELPER_TO_CLIENT);
    return;
  }

  switch (message.type)
  {
    case KVMFR_CLIPBOARD_MESSAGE_OFFER:
      m_cachedClipboard = message;
      m_cachedValid = true;
      m_helperFormats = message.token;
      m_helperToClient.Clear();
      m_replayPending = false;
      m_statusDirty = true;
      break;

    case KVMFR_CLIPBOARD_MESSAGE_CLEAR:
      m_cachedClipboard = message;
      m_cachedValid = true;
      m_helperFormats = 0;
      m_helperToClient.Clear();
      m_replayPending = false;
      m_statusDirty = true;
      break;

    case KVMFR_CLIPBOARD_MESSAGE_REQUEST:
      m_clientToHelper.Clear();
      m_discardClientToHelper = 0;
      m_clientToHelper.transfer = message.transfer;
      m_clientToHelper.clipboardGeneration =
        message.clipboardGeneration;
      m_clientToHelper.format = message.format;
      break;

    case KVMFR_CLIPBOARD_MESSAGE_DATA:
      AdvanceChunk(m_helperToClient, message);
      break;

    case KVMFR_CLIPBOARD_MESSAGE_CANCEL:
      if (m_clientToHelper.transfer == message.transfer)
        m_clientToHelper.Clear();
      if (m_helperToClient.transfer == message.transfer)
        m_helperToClient.Clear();
      break;
  }
}

bool CLGMPClipboardTransport::BeginTarget(
  const KVMFRClipboardMessage& message, bool acknowledge)
{
  if (m_pendingTarget.valid || message.length)
    return false;

  m_pendingTarget.valid       = true;
  m_pendingTarget.acknowledge = acknowledge;
  m_pendingTarget.record      = message;
  return RetryTarget();
}

void CLGMPClipboardTransport::FinishTarget(bool accepted)
{
  const KVMFRClipboardMessage message = m_pendingTarget.record;
  if (accepted)
  {
    ApplyInbound(message);
    if (m_ownerClientID && !m_ownerReleasing)
      RenewLease();
  }
  m_pendingTarget.Clear();

  if (!accepted)
  {
    ReleaseOwner("Helper delivery failed", false);
    m_failed = true;
  }
}

bool CLGMPClipboardTransport::RetryTarget()
{
  if (!m_pendingTarget.valid)
    return true;

  const bool acknowledge = m_pendingTarget.acknowledge;
  const ClipboardChannelResult result = m_target ?
    m_target->SendClipboard(m_pendingTarget.record, nullptr) :
    ClipboardChannelResult::FAILED;
  if (result == ClipboardChannelResult::BUSY)
  {
    if (m_ownerClientID)
      RenewLease();
    return true;
  }

  FinishTarget(result == ClipboardChannelResult::ACCEPTED);
  if (acknowledge)
  {
    const LGMP_STATUS status = lgmpHostAckData(m_queue);
    if (status != LGMP_OK)
    {
      Fail("lgmpHostAckData", status);
      return false;
    }
  }
  return true;
}

void CLGMPClipboardTransport::DropPendingTarget()
{
  if (!m_pendingTarget.valid)
    return;

  const bool acknowledge = m_pendingTarget.acknowledge;
  m_pendingTarget.Clear();
  if (acknowledge && m_queue)
  {
    const LGMP_STATUS status = lgmpHostAckData(m_queue);
    if (status != LGMP_OK)
      Fail("lgmpHostAckData", status);
  }
}

bool CLGMPClipboardTransport::ProcessMessage(
  uint32_t clientID, const KVMFRClipboardMessage& message)
{
  if (message.type == KVMFR_CLIPBOARD_MESSAGE_CLAIM)
  {
    if (!m_available || !ValidateClaim(message))
      return true;
    if (m_ownerClientID)
    {
      if (!m_ownerReleasing &&
          IsOwner(clientID, message.generation))
        RenewLease();
      return true;
    }

    if (!BindStreams(clientID))
    {
      DeInitStreams();
      m_failed = true;
      if (m_stopEvent)
        SetEvent(m_stopEvent);
      return true;
    }

    m_ownerClientID   = clientID;
    m_ownerGeneration = message.generation;
    RenewLease();
    m_statusDirty   = true;
    m_replayPending = m_cachedValid;
    DEBUG_INFO("Clipboard owner %u generation %u acquired",
      m_ownerClientID, m_ownerGeneration);
    return true;
  }

  if (!IsOwner(clientID, message.generation))
    return true;
  if (m_ownerReleasing)
    return true;

  if (CLGMPClipboardFiles::IsRecord(message) &&
      CLGMPClipboardFiles::DirectionValid(message,
        CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER) &&
      !m_files.TransferActive(message.transfer) &&
      (message.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE ||
       message.type == KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST) &&
      !ValidateInboundRecord(message))
  {
    // A cancellation or replacement can cross a request which was generated
    // from the preceding file offer. The already-posted lifecycle record will
    // cancel the client-side work; consuming this stale request avoids
    // treating normal cross-queue ordering as a protocol failure.
    RenewLease();
    return true;
  }
  if (!ValidateOwnedControl(message))
  {
    ReleaseOwner("invalid clipboard message", true);
    return true;
  }

  if (message.type == KVMFR_CLIPBOARD_MESSAGE_KEEPALIVE)
  {
    RenewLease();
    return true;
  }
  if (message.type == KVMFR_CLIPBOARD_MESSAGE_RELEASE)
  {
    ReleaseOwner("client release", true);
    return true;
  }

  if (m_files.IsStaleTerminal(message,
      CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER))
  {
    RenewLease();
    return true;
  }

  if (message.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST &&
      m_helperToClient.Active())
  {
    ReleaseOwner("overlapping clipboard request", true);
    return true;
  }
  if (message.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST &&
      (!m_cachedValid ||
       m_cachedClipboard.type != KVMFR_CLIPBOARD_MESSAGE_OFFER ||
       message.clipboardGeneration !=
         m_cachedClipboard.clipboardGeneration ||
       !(m_helperFormats & kvmfrClipboardFormatFlag(message.format))))
  {
    ReleaseOwner("invalid clipboard request", true);
    return true;
  }
  if (message.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL &&
      message.transfer != m_clientToHelper.transfer &&
      message.transfer != m_helperToClient.transfer)
    return true;
  KVMFRClipboardMessage forwarded = message;
  forwarded.generation = m_endpointGeneration;
  RenewLease();
  BeginTarget(forwarded, true);
  return false;
}

bool CLGMPClipboardTransport::DrainMessage()
{
  for (unsigned count = 0; count < 64 && !m_pendingTarget.valid; ++count)
  {
    uint8_t data[LGMP_MSGS_SIZE] = {};
    size_t size = 0;
    uint32_t clientID = 0;
    const LGMP_STATUS status = lgmpHostReadDataWithSource(
      m_queue, data, &size, &clientID);
    if (status == LGMP_ERR_QUEUE_EMPTY)
      return true;
    if (status != LGMP_OK)
    {
      Fail("lgmpHostReadDataWithSource", status);
      return false;
    }

    bool acknowledge = true;
    if (size != sizeof(KVMFRClipboardMessage))
    {
      if (clientID == m_ownerClientID)
        ReleaseOwner("invalid clipboard message size", true);
    }
    else
    {
      KVMFRClipboardMessage message = {};
      memcpy(&message, data, sizeof(message));
      acknowledge = ProcessMessage(clientID, message);
    }

    if (acknowledge)
    {
      const LGMP_STATUS ackStatus = lgmpHostAckData(m_queue);
      if (ackStatus != LGMP_OK)
      {
        Fail("lgmpHostAckData", ackStatus);
        return false;
      }
    }
  }
  return true;
}

ClipboardChannelResult CLGMPClipboardTransport::SendControl(
  const KVMFRClipboardMessage& record)
{
  PLGMPMemory memory = FindAvailable(m_messageMemory);
  if (!memory)
    return ClipboardChannelResult::BUSY;

  KVMFRClipboardMessage message = record;
  message.generation = m_ownerGeneration;
  memcpy(lgmpHostMemPtr(memory), &message, sizeof(message));
  const uint32_t serial = Seq::Next(m_messageSerial);
  const PostResult result = PostForOwner(
    KVMFR_CLIPBOARD_QUEUE_UDATA(
      KVMFR_CLIPBOARD_QUEUE_MESSAGE, serial), memory);
  if (result == PostResult::POSTED)
  {
    m_messageSerial = serial;
    ApplyOutbound(record);
    return ClipboardChannelResult::ACCEPTED;
  }
  if (result == PostResult::BUSY)
    return ClipboardChannelResult::BUSY;
  if (result == PostResult::GONE)
  {
    ReleaseOwner("subscriber disappeared", true, true);
    return record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER ||
      record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR ?
      ClipboardChannelResult::ACCEPTED : ClipboardChannelResult::BUSY;
  }
  return ClipboardChannelResult::FAILED;
}

ClipboardChannelResult CLGMPClipboardTransport::SendData(
  const KVMFRClipboardMessage& record, const uint8_t * data)
{
  const bool valid = CLGMPClipboardFiles::IsData(record) ?
    m_files.Validate(record,
      CLGMPClipboardFiles::Direction::HELPER_TO_CLIENT, 0, false) :
    ValidateChunk(m_helperToClient, record);
  if (!valid)
    return ClipboardChannelResult::FAILED;

  KVMFRClipboardMessage message = record;
  message.generation = m_ownerGeneration;
  LGMPStreamBuffer buffer = {};
  LGMP_STATUS status =
    lgmpHostStreamWriteAcquire(m_hostToClientStream, &buffer);
  if (status == LGMP_ERR_STREAM_FULL)
    return ClipboardChannelResult::BUSY;
  if (status == LGMP_ERR_STREAM_UNBOUND ||
      status == LGMP_ERR_STREAM_STALE)
  {
    ReleaseOwner("clipboard stream binding lost", true);
    return ClipboardChannelResult::ACCEPTED;
  }
  if (status != LGMP_OK)
  {
    Fail("lgmpHostStreamWriteAcquire", status);
    return ClipboardChannelResult::FAILED;
  }

  const uint32_t bytes = sizeof(message) + message.length;
  if (!buffer.data || buffer.capacity < bytes)
  {
    const LGMP_STATUS cancel =
      lgmpHostStreamWriteCancel(m_hostToClientStream, &buffer);
    if (cancel != LGMP_OK)
      DEBUG_ERROR("Failed to cancel an undersized host-to-client "
        "clipboard stream reservation: %s", lgmpStatusString(cancel));
    DEBUG_ERROR("Host-to-client clipboard stream returned an "
      "undersized slot: capacity=%u required=%u",
      buffer.capacity, bytes);
    m_failed = true;
    if (m_stopEvent)
      SetEvent(m_stopEvent);
    return ClipboardChannelResult::FAILED;
  }

  memcpy(buffer.data, &message, sizeof(message));
  if (message.length)
    WCWrite::Copy(static_cast<uint8_t *>(buffer.data) + sizeof(message), data,
      message.length);

  // Stream commit drains WC writes before publishing the producer cursor.
  status = lgmpHostStreamWriteCommit(
    m_hostToClientStream, &buffer, bytes);
  if (status == LGMP_OK)
  {
    ApplyOutbound(record);
    return ClipboardChannelResult::ACCEPTED;
  }

  DEBUG_ERROR("Failed to commit host-to-client clipboard stream data: %s",
    lgmpStatusString(status));
  if (status == LGMP_ERR_STREAM_UNBOUND ||
      status == LGMP_ERR_STREAM_STALE)
  {
    ReleaseOwner("clipboard stream binding lost", true);
    return ClipboardChannelResult::ACCEPTED;
  }
  Fail("lgmpHostStreamWriteCommit", status);
  return ClipboardChannelResult::FAILED;
}

ClipboardChannelResult CLGMPClipboardTransport::SendClipboard(
  const KVMFRClipboardMessage& record, const uint8_t * data)
{
  CSRWExclusiveLock lock(m_lock);
  if (m_failed || !m_target)
    return ClipboardChannelResult::FAILED;

  // The channel retains a BUSY record for an exact retry. Once the owner
  // epoch that caused the backpressure is gone, that record cannot be
  // delivered to a replacement owner. Consume it and promptly cancel a
  // stale request back toward Helper instead.
  if (BlockedOwnerLost(record))
  {
    QueueStaleRequestCancel(record);
    QueueStaleFileRecord(record);
    ClearOutboundBlock();
    Wake();
    return ClipboardChannelResult::ACCEPTED;
  }

  if (!m_available ||
      record.generation != m_endpointGeneration)
    return ClipboardChannelResult::FAILED;

  if (m_files.IsStaleTerminal(record,
      CLGMPClipboardFiles::Direction::HELPER_TO_CLIENT))
  {
    ClearOutboundBlock();
    return ClipboardChannelResult::ACCEPTED;
  }

  const bool validOutbound = ValidateOutboundRecord(record);
  if (!validOutbound && CLGMPClipboardFiles::IsRecord(record) &&
      CLGMPClipboardFiles::DirectionValid(record,
        CLGMPClipboardFiles::Direction::HELPER_TO_CLIENT) &&
      !m_files.TransferActive(record.transfer) &&
      (record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE ||
       record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST))
  {
    // The LGMP owner may disappear or replace its dataset before Helper
    // observes the matching lifecycle record. Fail new work locally without
    // tearing down the shared Helper clipboard channel.
    QueueStaleFileRecord(record);
    ClearOutboundBlock();
    Wake();
    return ClipboardChannelResult::ACCEPTED;
  }

  if (!validOutbound || (record.length && !data))
    return ClipboardChannelResult::FAILED;

  if (record.transfer &&
      record.transfer == m_discardHelperToClient &&
      (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA ||
       record.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL))
  {
    if (record.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL ||
        (record.flags & KVMFR_CLIPBOARD_FLAG_END))
      m_discardHelperToClient = 0;
    ClearOutboundBlock();
    return ClipboardChannelResult::ACCEPTED;
  }

  if (record.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL &&
      record.transfer != m_clientToHelper.transfer &&
      record.transfer != m_helperToClient.transfer)
  {
    ClearOutboundBlock();
    return ClipboardChannelResult::ACCEPTED;
  }

  // Preserve the latest Helper clipboard even when no client owns the
  // endpoint. It is replayed to the next successful claimant.
  if (record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER ||
      record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR)
  {
    m_cachedClipboard = record;
    m_cachedValid = true;
    m_helperFormats = record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER ?
      record.token : 0;
    m_statusDirty = true;
    if (!m_ownerClientID || m_ownerReleasing)
    {
      ClearOutboundBlock();
      Wake();
      return ClipboardChannelResult::ACCEPTED;
    }
  }

  if (!m_ownerClientID || m_ownerReleasing)
  {
    if (OwnerScopedLifecycle(record))
    {
      QueueStaleRequestCancel(record);
      QueueStaleFileRecord(record);
      ClearOutboundBlock();
      Wake();
      return ClipboardChannelResult::ACCEPTED;
    }
    return ClipboardChannelResult::FAILED;
  }

  const uint32_t ownerClientID = m_ownerClientID;
  const uint32_t ownerGeneration = m_ownerGeneration;
  if (!OwnerSubscribed())
  {
    BlockOutbound(record, ownerClientID, ownerGeneration);
    Wake();
    return ClipboardChannelResult::BUSY;
  }

  ClipboardChannelResult result;
  if (record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_DATA)
    result = SendData(record, data);
  else if (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA)
  {
    if (!m_helperToClient.Active() ||
        record.transfer != m_helperToClient.transfer)
    {
      ClearOutboundBlock();
      return ClipboardChannelResult::ACCEPTED;
    }
    result = SendData(record, data);
  }
  else
  {
    if (record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST &&
        m_clientToHelper.Active())
      return ClipboardChannelResult::FAILED;
    if (record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST &&
        (record.clipboardGeneration != m_clientClipboardGeneration ||
         !(m_clientFormats & kvmfrClipboardFormatFlag(record.format))))
    {
      QueueStaleRequestCancel(record);
      ClearOutboundBlock();
      return ClipboardChannelResult::ACCEPTED;
    }
    result = SendControl(record);
  }

  if (result == ClipboardChannelResult::BUSY)
    BlockOutbound(record, ownerClientID, ownerGeneration);
  else
    ClearOutboundBlock();
  Wake();
  return result;
}

void CLGMPClipboardTransport::ClipboardState(
  bool available, uint32_t generation)
{
  CSRWExclusiveLock lock(m_lock);
  if (!generation)
    return;

  if (generation != m_endpointGeneration || available != m_available)
  {
    DropPendingTarget();
    ClearStreamTargets();
    m_internalTargetCount = 0;
    for (KVMFRClipboardMessage& record : m_internalTarget)
      record = {};
    if (m_ownerClientID)
      ReleaseOwner("Helper endpoint changed", false);
    m_clientToHelper.Clear();
    m_helperToClient.Clear();
    m_files.Reset();
    m_clientClipboardGeneration = 0;
    m_clientFormats = 0;
    m_discardClientToHelper = 0;
    m_discardHelperToClient = 0;
    m_cachedClipboard = {};
    m_cachedValid = false;
    m_helperFormats = 0;
  }
  m_available = available;
  m_endpointGeneration = generation;
  m_statusDirty = true;
  Wake();
}

void CLGMPClipboardTransport::ClipboardReset(
  uint32_t generation, uint32_t reason)
{
  UNREFERENCED_PARAMETER(reason);
  CSRWExclusiveLock lock(m_lock);
  if (!generation)
    return;
  DropPendingTarget();
  ClearStreamTargets();
  m_internalTargetCount = 0;
  for (KVMFRClipboardMessage& record : m_internalTarget)
    record = {};
  if (m_ownerClientID)
    ReleaseOwner("Helper reset", false);
  m_clientToHelper.Clear();
  m_helperToClient.Clear();
  m_files.Reset();
  m_clientClipboardGeneration = 0;
  m_clientFormats = 0;
  m_discardClientToHelper = 0;
  m_discardHelperToClient = 0;
  m_cachedClipboard = {};
  m_cachedValid = false;
  m_helperFormats = 0;
  m_endpointGeneration = generation;
  m_statusDirty = true;
  Wake();
}

DWORD CALLBACK CLGMPClipboardTransport::ThreadProc(void * context)
{
  static_cast<CLGMPClipboardTransport *>(context)->Thread();
  return 0;
}

void CLGMPClipboardTransport::Thread()
{
  LGMPStreamPollState streamPoll = {};
  const LGMPStreamPollConfig pollConfig =
  {
    32U,
    50U,
    1000U,
  };
  const LGMP_STATUS pollStatus = lgmpStreamPollInit(&streamPoll,
    pollConfig);
  if (pollStatus != LGMP_OK)
  {
    DEBUG_ERROR("Failed to initialize LGMP clipboard polling: %s",
      lgmpStatusString(pollStatus));
    if (m_target)
      m_target->ClipboardFailed();
    return;
  }

  bool notifyFailed = false;
  for (;;)
  {
    uint32_t waitUs = IDLE_POLL_MS * 1000U;
    bool streamReceived = false;
    bool notifyReady = false;
    IClipboardTarget * readyTarget = nullptr;
    {
      CSRWExclusiveLock lock(m_lock);
      if (m_failed)
      {
        notifyFailed = true;
        break;
      }

      if (m_ownerClientID && !OwnerSubscribed())
      {
        DropPendingTarget();
        ClearStreamTargets();
        ReleaseOwner("subscriber disappeared", true, true);
      }
      else if (m_ownerClientID && !m_ownerReleasing &&
          GetTickCount64() >= m_ownerDeadline)
        ReleaseOwner("lease expired", true);

      // A restarted client can reuse dataset and transfer IDs. Deliver every
      // old-owner cleanup record before admitting messages from a new owner.
      if (!RetryTarget() || !RetryStreamTargets() ||
          !PumpInternalTarget() ||
          (!m_streamTargetCount && m_internalTargetCount == 0 &&
            !DrainMessage()) ||
          !DrainStream(streamReceived) ||
          !RetryOwnerRelease() ||
          !PublishStatus() ||
          !ReplayClipboard())
      {
        notifyFailed = true;
        break;
      }

      if (m_outboundBlocked)
      {
        notifyReady = m_target != nullptr;
        readyTarget = m_target;
      }
      if (m_pendingTarget.valid || m_internalTargetCount ||
          m_streamTargetCount || m_ownerClientID)
        waitUs = ACTIVE_POLL_MS * 1000U;
      if (m_ownerClientID)
      {
        if (streamReceived)
          lgmpStreamPollActivity(&streamPoll);
        waitUs = lgmpStreamPollIdle(&streamPoll);
      }
      else
        lgmpStreamPollActivity(&streamPoll);
    }

    // ClipboardReceiveReady may synchronously retry SendClipboard, which
    // acquires m_lock. Never invoke it while holding the transport lock.
    if (notifyReady)
      readyTarget->ClipboardReceiveReady();

    if (!waitUs)
      continue;
    if (!ArmPollTimer(m_pollTimer, waitUs))
    {
      const DWORD error = GetLastError();
      if (WaitForSingleObject(m_stopEvent, 0) == WAIT_OBJECT_0)
        break;
      DEBUG_ERROR_HR(error,
        "Failed to arm LGMP clipboard poll timer");
      notifyFailed = true;
      break;
    }

    const HANDLE handles[] =
    {
      m_stopEvent,
      m_wakeEvent,
      m_pollTimer,
    };
    const DWORD wait = WaitForMultipleObjects(
      _countof(handles), handles, FALSE, INFINITE);
    if (wait == WAIT_OBJECT_0)
      break;
    if (wait == WAIT_OBJECT_0 + 1)
    {
      lgmpStreamPollActivity(&streamPoll);
      continue;
    }
    if (wait == WAIT_OBJECT_0 + 2)
      continue;
    if (wait == WAIT_FAILED)
    {
      DEBUG_ERROR_HR(GetLastError(),
        "LGMP clipboard worker wait failed");
      notifyFailed = true;
      break;
    }
    DEBUG_ERROR("LGMP clipboard worker returned an unexpected wait "
      "result: %lu", wait);
    notifyFailed = true;
    break;
  }

  IClipboardTarget * target = nullptr;
  {
    CSRWExclusiveLock lock(m_lock);
    DropPendingTarget();
    ClearStreamTargets();
    if (m_ownerClientID)
      ReleaseOwner("transport stopped", false, true);
    target = m_target;
  }
  if (notifyFailed && target)
    target->ClipboardFailed();
}
