// vi:ft=cpp
/* SPDX-License-Identifier: MIT */
/*
 * Copyright (C) 2015-2020, 2022, 2024 Kernkonzept GmbH.
 * Author(s): Sarah Hoffmann <sarah.hoffmann@kernkonzept.com>
 *
 */
#pragma once

#include <l4/sys/factory>
#include <l4/sys/semaphore>
#include <l4/re/dataspace>
#include <l4/re/env>
#include <l4/re/util/unique_cap>
#include <l4/re/util/object_registry>
#include <l4/re/error_helper>

#include <l4/util/atomic.h>
#include <l4/util/bitops.h>
#include <l4/l4virtio/l4virtio>
#include <l4/l4virtio/virtqueue>
#include <l4/sys/consts.h>

#include <cstring>

namespace L4virtio { namespace Driver {

/**
 * \brief Client-side implementation for a general virtio device.
 */
class Device
{
public:
  /**
   * Contacts the device and starts the initial handshake.
   *
   * \param srvcap         Capability for device communication.
   * \param manage_notify  Set up a semaphore for notifications from
   *                       the device. See below.
   *
   * \throws L4::Runtime_error if the initialisation fails
   *
   * This function contacts the server, sets up the notification
   * channels and the configuration dataspace. After this is done,
   * the caller can set up any dataspaces it needs. The initialisation
   * then needs to be finished by calling driver_acknowledge().
   *
   * Per default this function creates and registers a semaphore for receiving
   * notification from the device. This semaphore is used in the blocking
   * functions send_and_wait(), wait() and next_used().
   *
   * When `manage_notify` is false, then the caller may manually register
   * and handle notification interrupts from the device. This is for example
   * useful, when the client runs in an application with a server loop.
   */
  void driver_connect(L4::Cap<L4virtio::Device> srvcap, bool manage_notify = true)
  {
    _device = srvcap;

    _next_devaddr = L4_SUPERPAGESIZE;

    auto *e = L4Re::Env::env();

    // Set up the virtio configuration page.

    _config_cap = L4Re::chkcap(L4Re::Util::make_unique_cap<L4Re::Dataspace>(),
                               "Allocate config dataspace capability");

    l4_addr_t ds_offset;
    L4Re::chksys(_device->device_config(_config_cap.get(), &ds_offset),
                 "Request virtio config page");

    if (ds_offset & ~L4_PAGEMASK)
      L4Re::chksys(-L4_EINVAL, "Virtio config page is page aligned.");

    L4Re::chksys(e->rm()->attach(&_config, L4_PAGESIZE,
                                 L4Re::Rm::F::Search_addr | L4Re::Rm::F::RW,
                                 L4::Ipc::make_cap_rw(_config_cap.get()), ds_offset,
                                 L4_PAGESHIFT),
                 "Attach config dataspace");

    if (memcmp(&_config->magic, "virt", 4) != 0)
      L4Re::chksys(-L4_ENODEV, "Device config has wrong magic value");

    if (_config->version != 2)
      L4Re::chksys(-L4_ENODEV, "Invalid virtio version, must be 2");

    _device->set_status(0); // reset
    int status = L4VIRTIO_STATUS_ACKNOWLEDGE;
    _device->set_status(status);

    status |= L4VIRTIO_STATUS_DRIVER;
    _device->set_status(status);

    if (_config->fail_state())
      L4Re::chksys(-L4_EIO, "Device failure during initialisation.");

    // Set up the interrupt used to notify the device about events.
    // (only supporting one interrupt with index 0 at the moment)

    _host_irq = L4Re::chkcap(L4Re::Util::make_unique_cap<L4::Irq>(),
                             "Allocate host IRQ capability");

    L4Re::chksys(_device->device_notification_irq(0, _host_irq.get()),
                 "Request device notification interrupt.");

    // Set up the interrupt to get notifications from the device.
    // (only supporting one interrupt with index 0 at the moment)
    if (manage_notify)
      {
        _driver_notification =
          L4Re::chkcap(L4Re::Util::make_unique_cap<L4::Semaphore>(),
                       "Allocate notification capability");

        L4Re::chksys(l4_error(e->factory()->create(_driver_notification.get())),
                     "Create semaphore for notifications from device");

        L4Re::chksys(_device->bind(0, _driver_notification.get()),
                     "Bind driver notification interrupt");
      }
  }

  /**
   * Register a triggerable to receive notifications from the device.
   *
   * \param      index  Index of the interrupt.
   * \param[out] irq    Triggerable to register for notifications.
   */
  int bind_notification_irq(unsigned index, L4::Cap<L4::Triggerable> irq) const
  { return l4_error(_device->bind(index, irq)); }

  /// Return true if the device is in a fail state.
  bool fail_state() const { return _config->fail_state(); }

  /**
   * Check if a particular feature bit was negotiated with the device.
   * The result is only valid after driver_acknowledge() was called
   * (when the handshake with the device was completed).
   *
   * \param feat  The feature bit.
   *
   * \retval true   The feature is supported by both driver and device.
   * \retval false  The feature is not supported by the driver and/or device.
   */
  bool feature_negotiated(unsigned int feat) const
  { return l4virtio_get_feature(_config->driver_features_map, feat); }

  /**
   * Finalize handshake with the device.
   *
   * Must be called after all queues have been set up and before the first
   * request is sent. It is still possible to add more shared dataspaces
   * after the handshake has been finished.
   *
   */
  int driver_acknowledge()
  {
    if (!l4virtio_get_feature(_config->dev_features_map,
                              L4VIRTIO_FEATURE_VERSION_1))
      L4Re::chksys(-L4_ENODEV,
                   "Require Virtio 1.0 device; Legacy device not supported.");

    _config->driver_features_map[0] &= _config->dev_features_map[0];
    _config->driver_features_map[1] &= _config->dev_features_map[1];

    _device->set_status(_config->status | L4VIRTIO_STATUS_FEATURES_OK);

    if (!(_config->status & L4VIRTIO_STATUS_FEATURES_OK))
      L4Re::chksys(-L4_EINVAL, "Negotiation of device features.");

    _device->set_status(_config->status | L4VIRTIO_STATUS_DRIVER_OK);

    if (_config->fail_state())
      return -L4_EIO;

    return L4_EOK;
  }

  /**
   * Share a dataspace with the device.
   *
   * \param ds      Dataspace to share with the device.
   * \param offset  Offset in dataspace where the shared part starts.
   * \param size    Total size in bytes of the shared space.
   * \param devaddr Start of shared space in the device address space.
   *
   * Although this function allows to share only a part of the given dataspace
   * for convenience, the granularity of sharing is always the dataspace level.
   * Thus, the remainder of the dataspace is not protected from the device.
   *
   * When communicating with the device, addresses must be given with respect
   * to the device address space. This is not the same as the virtual address
   * space of the client in order to not leak information about the address
   * space layout.
   */
  int register_ds(L4::Cap<L4Re::Dataspace> ds, l4_umword_t offset,
                  l4_umword_t size, l4_uint64_t *devaddr)
  {
    *devaddr = next_device_address(size);
    return _device->register_ds(L4::Ipc::make_cap_rw(ds), *devaddr, offset, size);
  }

  /**
   * Send the virtqueue configuration to the device.
   *
   * \param  num         Number of queue to configure.
   * \param  size        Size of rings in the queue, must be a power of 2)
   * \param  desc_addr   Address of descriptor table (device address)
   * \param  avail_addr  Address of available ring (device address)
   * \param  used_addr   Address of used ring (device address)
   */
  int config_queue(int num, unsigned size, l4_uint64_t desc_addr,
                   l4_uint64_t avail_addr, l4_uint64_t used_addr)
  {
    auto *queueconf = &_config->queues()[num];
    queueconf->num = size;
    queueconf->desc_addr = desc_addr;
    queueconf->avail_addr = avail_addr;
    queueconf->used_addr = used_addr;
    queueconf->ready = 1;

    return _device->config_queue(num);
  }

  /**
   * Maximum queue size allowed by the device.
   *
   * \param num  Number of queue for which to determine the maximum size.
   */
  int max_queue_size(int num) const
  {
    return _config->queues()[num].num_max;
  }

  /**
   * Send a request to the device and wait for it to be processed.
   *
   * \param queue  Queue that contains the request in its descriptor table
   * \param descno Index of first entry in descriptor table where
   *
   * This function provides a simple mechanism to send requests
   * synchronously. It must not be used with other requests at the same
   * time as it directly waits for a notification on the device irq cap.
   *
   * \pre driver_connect() was called with manage_notify.
   */
  int send_and_wait(Virtqueue &queue, l4_uint16_t descno)
  {
    send(queue, descno);

    // wait for a reply, we assume that no other
    // request will get in the way.
    auto head = wait_for_next_used(queue);

    if (head < 0)
      return head;

    return (head == descno) ? L4_EOK : -L4_EINVAL;
  }

  /**
   * Wait for a notification from the device.
   *
   * \param index  Notification slot to wait for.
   *
   * \pre driver_connect() was called with manage_notify.
   */
  int wait(int index) const
  {
    if (index != 0)
      return -L4_EEXIST;

    return l4_ipc_error(_driver_notification->down(), l4_utcb());
  }

  /**
   * Wait for the next item to arrive in the used queue and return it.
   *
   * \param queue     A queue.
   * \param[out] len  (optional) Size of valid data in finished block.
   *                  Note that this is the value reported by the device,
   *                  which may set it to a value that is larger than the
   *                  original buffer size.
   * \retval >=0  Descriptor number of item removed from used queue.
   * \retval <0   IPC error while waiting for notification.
   *
   * The call blocks until the next item is available in the used queue.
   *
   * \pre driver_connect() was called with manage_notify.
   */
  int wait_for_next_used(Virtqueue &queue, l4_uint32_t *len = nullptr) const
  {
    while (true)
      {
        int err = wait(0);

        if (err < 0)
          return err;

        auto head = queue.find_next_used(len);
        if (head != Virtqueue::Eoq) // spurious interrupt?
          return head;
      }
  }

  /**
   * Send a request to the device.
   *
   * \param queue  Queue that contains the request in its descriptor table
   * \param descno Index of first entry in descriptor table where
   */
  void send(Virtqueue &queue, l4_uint16_t descno)
  {
    queue.enqueue_descriptor(descno);
    notify(queue);
  }

  void notify(Virtqueue &queue)
  {
    if (!queue.no_notify_host())
      _host_irq->trigger();
  }

private:
  /**
   * Get the next free address, covering the given area.
   *
   * \param size  Size of requested area.
   *
   * Builds up a virtual address space for the device.
   * Simply give out the memory linearly, it is unlikely that a client
   * wants to map more than 4GB and it certainly shouldn't reallocate all the
   * time.
   */
  l4_uint64_t next_device_address(l4_umword_t size)
  {
    l4_umword_t ret;
    size = l4_round_page(size);
    do
      {
        ret = _next_devaddr;
        if (l4_umword_t(~0) - ret < size)
          L4Re::chksys(-L4_ENOMEM, "Out of device address space.");
      }
    while (!l4util_cmpxchg(&_next_devaddr, ret, ret + size));

    return ret;
  }

protected:
  L4::Cap<L4virtio::Device> _device;
  L4Re::Rm::Unique_region<L4virtio::Device::Config_hdr *> _config;
  l4_umword_t _next_devaddr;
  L4Re::Util::Unique_cap<L4::Semaphore> _driver_notification;

private:
  L4Re::Util::Unique_cap<L4::Irq> _host_irq;
  L4Re::Util::Unique_cap<L4Re::Dataspace> _config_cap;
};

} }
