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

#include "common/debug.h"
#include "common/time.h"

#include <usbredirparser.h>

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

#define USB_REDIR_CHANNEL_COUNT 256
#define USB_REDIR_DISCONNECT_TIMEOUT_NS INT64_C(500000000)
#define USB_REDIR_RECONNECT_DELAY_NS    INT64_C(250000000)
#define USB_REDIR_MAX_OUTPUT_BYTES      UINT64_C(1048576)

struct LG_USBRedir
{
  const LG_USBRedirDeviceOps * deviceOps;
  void                       * deviceOpaque;
  LG_USBRedirStatusFn          status;
  void                       * statusOpaque;

  PSUSBRedirChannel * channels[USB_REDIR_CHANNEL_COUNT];
  PSUSBRedirChannel * channel;
  struct usbredirparser * parser;

  const uint8_t * readData;
  size_t          readSize;
  size_t          readOffset;

  atomic_bool desiredPlugged;
  atomic_bool available;
  bool        plugged;
  bool        disconnectPending;
  int64_t     disconnectDeadline;
  int64_t     reconnectDeadline;
  uint64_t    mainPingCount;
  bool        waitingForMainPing;
};

static void setAvailable(LG_USBRedir * usbredir, bool available)
{
  const bool previous = atomic_exchange_explicit(
      &usbredir->available, available, memory_order_acq_rel);
  if (previous != available && usbredir->status)
    usbredir->status(usbredir->statusOpaque, available);
}

static int readUSBRedir(void * opaque, uint8_t * data, int count)
{
  LG_USBRedir * usbredir = opaque;
  const size_t remaining = usbredir->readSize - usbredir->readOffset;
  if (remaining < (size_t)count)
    count = (int)remaining;

  if (!count)
    return 0;

  memcpy(data, usbredir->readData + usbredir->readOffset, count);
  usbredir->readOffset += count;
  return count;
}

static int writeUSBRedir(void * opaque, uint8_t * data, int count)
{
  LG_USBRedir * usbredir = opaque;
  if (!usbredir->channel)
    return -1;

  const ssize_t written = purespice_usbRedirWriteNonblocking(
      usbredir->channel, data, count);
  return written >= 0 && written <= count ? (int)written : -1;
}

static void logUSBRedir(void * opaque, int level, const char * message)
{
  (void)opaque;
  switch (level)
  {
    case usbredirparser_error:
      DEBUG_ERROR("USB redirection: %s", message);
      break;

    case usbredirparser_warning:
      DEBUG_WARN("USB redirection: %s", message);
      break;

    case usbredirparser_info:
      DEBUG_INFO("USB redirection: %s", message);
      break;

    case usbredirparser_debug:
    case usbredirparser_debug_data:
      DEBUG_TRACE("USB redirection: %s", message);
      break;

    case usbredirparser_none:
      break;
  }
}

static void helloUSBRedir(void * opaque,
    struct usb_redir_hello_header * hello)
{
  (void)hello;
  LG_USBRedir * usbredir = opaque;
  setAvailable(usbredir, true);
}

static void deviceDisconnectAck(void * opaque)
{
  LG_USBRedir * usbredir = opaque;
  usbredir->disconnectPending = false;
  usbredir->disconnectDeadline = 0;
}

static void unplugDevice(LG_USBRedir * usbredir)
{
  if (!usbredir->plugged)
    return;

  usbredir->deviceOps->unplug(usbredir->deviceOpaque);
  usbredir->plugged = false;
}

static void destroyParser(LG_USBRedir * usbredir)
{
  setAvailable(usbredir, false);
  unplugDevice(usbredir);
  usbredir->disconnectPending = false;
  usbredir->disconnectDeadline = 0;

  if (!usbredir->parser)
    return;

  usbredirparser_destroy(usbredir->parser);
  usbredir->parser = NULL;
}

static bool flushUSBRedir(LG_USBRedir * usbredir)
{
  if (!usbredir->parser)
    return true;

  if (usbredirparser_do_write(usbredir->parser) != 0)
    return false;

  const uint64_t buffered =
    usbredirparser_get_bufferered_output_size(usbredir->parser);
  if (buffered <= USB_REDIR_MAX_OUTPUT_BYTES)
    return true;

  DEBUG_ERROR("USB redirection output queue exceeded %u bytes",
      (unsigned int)USB_REDIR_MAX_OUTPUT_BYTES);
  return false;
}

static bool connectChannel(LG_USBRedir * usbredir,
    PSUSBRedirChannel * channel)
{
  usbredir->channel = channel;
  if (purespice_usbRedirConnect(channel))
    return true;

  DEBUG_WARN("Failed to connect USB redirection channel %u",
      purespice_usbRedirChannelId(channel));
  usbredir->channel = NULL;
  return false;
}

static bool selectChannel(LG_USBRedir * usbredir)
{
  if (usbredir->channel)
    return true;

  if (!purespice_channelConnected(PS_CHANNEL_MAIN))
  {
    usbredir->reconnectDeadline =
      (int64_t)nanotime() + USB_REDIR_RECONNECT_DELAY_NS;
    return false;
  }

  if (usbredir->waitingForMainPing)
  {
    if (purespice_mainPingCount() == usbredir->mainPingCount)
    {
      usbredir->reconnectDeadline =
        (int64_t)nanotime() + USB_REDIR_RECONNECT_DELAY_NS;
      return false;
    }

    usbredir->waitingForMainPing = false;
  }

  for (unsigned int i = 0; i < USB_REDIR_CHANNEL_COUNT; ++i)
  {
    PSUSBRedirChannel * channel = usbredir->channels[i];
    if (!channel || !purespice_usbRedirAvailable(channel))
      continue;

    if (connectChannel(usbredir, channel))
    {
      usbredir->reconnectDeadline = 0;
      return true;
    }
  }

  usbredir->reconnectDeadline =
    (int64_t)nanotime() + USB_REDIR_RECONNECT_DELAY_NS;
  return false;
}

static void resetChannel(LG_USBRedir * usbredir)
{
  PSUSBRedirChannel * channel = usbredir->channel;
  usbredir->channel = NULL;
  destroyParser(usbredir);

  if (channel && purespice_usbRedirConnected(channel))
    purespice_usbRedirDisconnect(channel);

  usbredir->reconnectDeadline =
    (int64_t)nanotime() + USB_REDIR_RECONNECT_DELAY_NS;
}

static bool createParser(LG_USBRedir * usbredir)
{
  if (usbredir->parser)
    return true;

  usbredir->parser = usbredirparser_create();
  if (!usbredir->parser)
  {
    DEBUG_ERROR("Failed to create the USB redirection parser");
    return false;
  }

  usbredir->deviceOps->setup(usbredir->deviceOpaque, usbredir->parser);
  usbredir->parser->priv                       = usbredir;
  usbredir->parser->log_func                   = logUSBRedir;
  usbredir->parser->read_func                  = readUSBRedir;
  usbredir->parser->write_func                 = writeUSBRedir;
  usbredir->parser->hello_func                 = helloUSBRedir;
  usbredir->parser->device_disconnect_ack_func = deviceDisconnectAck;

  uint32_t caps[USB_REDIR_CAPS_SIZE] = { 0 };
  usbredirparser_caps_set_cap(caps,
      usb_redir_cap_device_disconnect_ack);
  usbredirparser_caps_set_cap(caps,
      usb_redir_cap_connect_device_version);
  usbredirparser_caps_set_cap(caps,
      usb_redir_cap_ep_info_max_packet_size);
  usbredirparser_caps_set_cap(caps, usb_redir_cap_64bits_ids);
  usbredirparser_caps_set_cap(caps, usb_redir_cap_32bits_bulk_length);
  usbredirparser_init(usbredir->parser, "Looking Glass", caps,
      USB_REDIR_CAPS_SIZE, usbredirparser_fl_usb_host);

  if (flushUSBRedir(usbredir))
    return true;

  DEBUG_ERROR("Failed to send the USB redirection hello packet");
  destroyParser(usbredir);
  return false;
}

LG_USBRedir * lgUsbRedir_create(
    const LG_USBRedirDeviceOps * deviceOps, void * deviceOpaque,
    LG_USBRedirStatusFn status, void * statusOpaque)
{
  if (!deviceOps || !deviceOps->setup || !deviceOps->plug ||
      !deviceOps->unplug)
    return NULL;

  LG_USBRedir * usbredir = calloc(1, sizeof(*usbredir));
  if (!usbredir)
    return NULL;

  usbredir->deviceOps      = deviceOps;
  usbredir->deviceOpaque   = deviceOpaque;
  usbredir->status         = status;
  usbredir->statusOpaque   = statusOpaque;
  atomic_init(&usbredir->desiredPlugged, false);
  atomic_init(&usbredir->available, false);
  return usbredir;
}

void lgUsbRedir_destroy(LG_USBRedir * usbredir)
{
  if (!usbredir)
    return;

  destroyParser(usbredir);
  free(usbredir);
}

void lgUsbRedir_setPlugged(LG_USBRedir * usbredir, bool plugged)
{
  atomic_store_explicit(&usbredir->desiredPlugged, plugged,
      memory_order_release);
}

bool lgUsbRedir_available(const LG_USBRedir * usbredir)
{
  return atomic_load_explicit(&usbredir->available, memory_order_acquire);
}

bool lgUsbRedir_disconnectPending(const LG_USBRedir * usbredir)
{
  return usbredir->disconnectPending;
}

static bool processUSBRedir(LG_USBRedir * usbredir, bool recover)
{
  const int64_t now = (int64_t)nanotime();
  if (!usbredir->channel &&
      now >= usbredir->reconnectDeadline)
    selectChannel(usbredir);

  if (!usbredir->parser ||
      !atomic_load_explicit(&usbredir->available, memory_order_acquire))
  {
    const bool result = flushUSBRedir(usbredir);
    if (!result && recover)
      resetChannel(usbredir);
    return result;
  }

  bool result = flushUSBRedir(usbredir);
  if (!result)
  {
    if (recover)
      resetChannel(usbredir);
    return false;
  }

  if (usbredir->disconnectPending)
  {
    if (usbredir->disconnectDeadline &&
        now >= usbredir->disconnectDeadline)
    {
      DEBUG_WARN("USB redirection disconnect acknowledgement timed out");
      resetChannel(usbredir);
      return true;
    }

    return true;
  }

  const bool desired = atomic_load_explicit(&usbredir->desiredPlugged,
      memory_order_acquire);
  if (desired != usbredir->plugged)
  {
    if (desired)
    {
      usbredir->deviceOps->plug(
          usbredir->deviceOpaque, usbredir->parser);
      usbredir->plugged = true;
    }
    else
    {
      unplugDevice(usbredir);
      usbredir->disconnectPending = usbredirparser_peer_has_cap(
          usbredir->parser, usb_redir_cap_device_disconnect_ack);
      usbredir->disconnectDeadline = usbredir->disconnectPending ?
        now + USB_REDIR_DISCONNECT_TIMEOUT_NS : 0;
      usbredirparser_send_device_disconnect(usbredir->parser);
    }

    result = flushUSBRedir(usbredir);
    if (!result)
    {
      if (recover)
        resetChannel(usbredir);
      return false;
    }
  }

  if (usbredir->plugged && usbredir->deviceOps->process)
  {
    const bool writable =
      usbredirparser_has_data_to_write(usbredir->parser) == 0 &&
      purespice_usbRedirWritable(usbredir->channel);
    usbredir->deviceOps->process(usbredir->deviceOpaque, writable);
  }

  result = flushUSBRedir(usbredir);
  if (!result && recover)
    resetChannel(usbredir);
  return result;
}

bool lgUsbRedir_process(LG_USBRedir * usbredir)
{
  return processUSBRedir(usbredir, true);
}

void lgUsbRedir_state(PSUSBRedirChannel * channel,
    PSUSBRedirState state, void * opaque)
{
  LG_USBRedir * usbredir = opaque;
  const uint8_t id = purespice_usbRedirChannelId(channel);

  switch (state)
  {
    case PS_USB_REDIR_AVAILABLE:
      usbredir->channels[id] = channel;
      selectChannel(usbredir);
      break;

    case PS_USB_REDIR_CONNECTED:
      if (channel != usbredir->channel)
      {
        purespice_usbRedirDisconnect(channel);
        break;
      }

      if (!createParser(usbredir))
        purespice_usbRedirDisconnect(channel);
      break;

    case PS_USB_REDIR_DISCONNECTED:
      if (channel == usbredir->channel)
      {
        destroyParser(usbredir);
        usbredir->channel = NULL;
        if (purespice_usbRedirAvailable(channel))
        {
          /* Wait for a fresh MAIN PING/PONG before reconnecting this
           * optional channel. The server shuts it down before MAIN during a
           * session teardown, so this prevents reconnect attempts to a
           * disappearing SPICE server. */
          usbredir->mainPingCount = purespice_mainPingCount();
          usbredir->waitingForMainPing = true;
          usbredir->reconnectDeadline =
            (int64_t)nanotime() + USB_REDIR_RECONNECT_DELAY_NS;
        }
      }
      break;

    case PS_USB_REDIR_UNAVAILABLE:
      if (usbredir->channels[id] == channel)
        usbredir->channels[id] = NULL;

      if (channel == usbredir->channel)
      {
        destroyParser(usbredir);
        usbredir->channel = NULL;
        selectChannel(usbredir);
      }
      break;
  }
}

bool lgUsbRedir_data(PSUSBRedirChannel * channel,
    const uint8_t * data, size_t size, void * opaque)
{
  LG_USBRedir * usbredir = opaque;
  if (channel != usbredir->channel || !usbredir->parser ||
      usbredir->readData)
    return false;

  usbredir->readData   = data;
  usbredir->readSize   = size;
  usbredir->readOffset = 0;

  const int result = usbredirparser_do_read(usbredir->parser);
  const bool consumed = usbredir->readOffset == usbredir->readSize;
  usbredir->readData   = NULL;
  usbredir->readSize   = 0;
  usbredir->readOffset = 0;

  if (result == usbredirparser_read_parse_error)
  {
    DEBUG_ERROR("Invalid USB redirection packet");
    return false;
  }

  if (!consumed)
  {
    DEBUG_ERROR("USB redirection parser did not consume its input");
    return false;
  }

  /* Returning false lets PureSpice disconnect after this callback unwinds;
   * resetting it here would destroy the channel during its own dispatch. */
  return processUSBRedir(usbredir, false);
}

void * lgUsbRedir_device(void * parserOpaque)
{
  LG_USBRedir * usbredir = parserOpaque;
  return usbredir->deviceOpaque;
}
