// vi:set ft=cpp: -*- Mode: C++ -*-
/* SPDX-License-Identifier: MIT */
/*
 * Copyright (C) 2013-2024 Kernkonzept GmbH.
 * Author(s): Alexander Warg <alexander.warg@kernkonzept.com>
 *            Matthias Lange <matthias.lange@kernkonzept.com>
 *
 */
#pragma once

#include "virtio.h"
#include <l4/sys/capability>
#include <l4/sys/cxx/ipc_client>
#include <l4/re/dataspace>
#include <l4/sys/irq>
#include <l4/cxx/utils>

namespace L4virtio {
/**
 * IPC interface for virtio over L4 IPC.
 *
 * The L4virtio protocol is an adaption of the mmio virtio transport 1.0(4).
 * This interface allows to exchange the necessary resources: device
 * configuration page, notification interrupts and dataspaces for payload.
 *
 * Notification interrupts can be configured independently for changes to
 * the configuration space and each queue through special L4virtio-specific
 * notify_index fields in the config page and queue configuration. The
 * interface distinguishes between device-to-driver and driver-to-device
 * notification interrupts.
 *
 * Device-to-driver interrupts are configured via the ICU interface. The
 * device announces the maximum number of supported interrupts via Icu::info().
 * The driver can then bind interrupts using Icu::bind().
 *
 * Driver-to-device interrupts must be requested from the device through
 * device_notification_irq().
 */
class Device :
  public L4::Kobject_t<Device, L4::Icu, L4VIRTIO_PROTOCOL,
                       L4::Type_info::Demand_t<1> >
{
public:
  typedef l4virtio_config_queue_t Config_queue;
  struct Config_hdr : l4virtio_config_hdr_t
  {
    Config_queue *queues() const
    { return l4virtio_config_queues(this); }

    template <typename T>
    T *device_config() const
    {
      return static_cast<T*>(l4virtio_device_config(this));
    }

    int config_queue(unsigned num, L4::Cap<L4::Triggerable> out_notify,
                     L4::Cap<L4::Triggerable> in_notify,
                     l4_timeout_s to = L4_IPC_TIMEOUT_NEVER)
    {
      return send_cmd(L4VIRTIO_CMD_CFG_QUEUE | num,
                      out_notify, in_notify, to);
    }

    int notify_queue(unsigned num, L4::Cap<L4::Triggerable> out_notify,
                     L4::Cap<L4::Triggerable> in_notify,
                     l4_timeout_s to = L4_IPC_TIMEOUT_NEVER)
    {
      return send_cmd(L4VIRTIO_CMD_NOTIFY_QUEUE | num,
                      out_notify, in_notify, to);
    }

    /**
     * Check the VIRTIO status register device and driver failure bits.
     *
     * \return true if either FAILED or DEVICE_NEEDS_RESET is set.
     *
     * Called by driver-side code to read the shared memory status word and
     * check the fail state of both the device and driver.
     */
    bool fail_state() const
    {
      auto cfg_status = cxx::access_once(&status);
      return cfg_status
             & (L4VIRTIO_STATUS_FAILED | L4VIRTIO_STATUS_DEVICE_NEEDS_RESET);
    }

    int set_status(unsigned new_status, L4::Cap<L4::Triggerable> out_notify,
                   L4::Cap<L4::Triggerable> in_notify,
                   l4_timeout_s to = L4_IPC_TIMEOUT_NEVER)
    {
      return send_cmd(L4VIRTIO_CMD_SET_STATUS | new_status,
                      out_notify, in_notify, to);
    }

    int cfg_changed(unsigned reg, L4::Cap<L4::Triggerable> out_notify,
                   L4::Cap<L4::Triggerable> in_notify,
                   l4_timeout_s to = L4_IPC_TIMEOUT_NEVER)
    {
      return send_cmd(L4VIRTIO_CMD_CFG_CHANGED | reg,
                      out_notify, in_notify, to);
    }

    int send_cmd(unsigned command, L4::Cap<L4::Triggerable> out_notify,
                 L4::Cap<L4::Triggerable> in_notify,
                 l4_timeout_s to = L4_IPC_TIMEOUT_NEVER)
    {
      cxx::write_now(&cmd, command);

      if (out_notify)
        out_notify->trigger();

      auto utcb = l4_utcb();
      auto ipc_to = l4_timeout(L4_IPC_TIMEOUT_0, to);

      do
        {
          if (in_notify)
            if (l4_ipc_error(l4_ipc_receive(in_notify.cap(), utcb, ipc_to),
                             utcb) == L4_IPC_RETIMEOUT)
              break;
        }
      while (cxx::access_once(&cmd));

      return cxx::access_once(&cmd) ? -L4_EBUSY : L4_EOK;
    }
  };

  /**
   * Write the VIRTIO status register.
   *
   * \param status  Status word to write to the VIRTIO status.
   *
   * \retval 0 on success.
   *
   * \note All other registers are accessed via shared memory.
   */
  L4_INLINE_RPC_OP(L4VIRTIO_OP_SET_STATUS, long,
                   set_status, (unsigned status));

  /**
   * Trigger queue configuration of the given queue.
   *
   * Usually all queues are configured when the status is written to running.
   * However, in some cases queues shall be disabled or enabled dynamically, in
   * this case this function triggers a reconfiguration from the shared memory
   * register of the queue config.
   *
   * \param queue  Queue index for the queue to be configured.
   *
   * \retval 0 on success.
   * \retval -L4_EIO    The queue's status is invalid.
   * \retval -L4_ERANGE The queue index exceeds the number of queues.
   * \retval -L4_EINVAL Otherwise.
   */
  L4_INLINE_RPC_OP(L4VIRTIO_OP_CONFIG_QUEUE, long,
                   config_queue, (unsigned queue));

  /**
   * Register a shared data space with VIRTIO host
   *
   * \param ds_cap  Dataspace capability to register. The lower 8 bits determine
   *                the rights mask with which the guest's rights are masked during
   *                the registration of the dataspace at the VIRTIO host.
   * \param base    VIRTIO guest physical start address of shared memory region
   * \param offset  Offset within the data space that is attached to the
   *                given `base` in the guest physical memory.
   * \param size    Size of the memory region in the guest
   *
   * \retval L4_EOK      Operation successful.
   * \retval -L4_EINVAL  The `ds_cap` capability is invalid, does not refer to
   *                     a valid dataspace, is not a trusted dataspace if
   *                     trusted dataspace validation is enabled, or `size` and
   *                     `offset` specify an invalid region.
   * \retval -L4_ENOMEM  The limit of dataspaces that can be registered has been
   *                     reached or no capability slot could be allocated.
   * \retval -L4_ERANGE  `offset` is lager than the size of the dataspace.
   * \retval <0          Any error returned by the dataspace when queried for
   *                     information during setup or any error returned by the
   *                     region manager from attaching the dataspace.
   */
  L4_INLINE_RPC_OP(L4VIRTIO_OP_REGISTER_DS, long,
                   register_ds, (L4::Ipc::Cap<L4Re::Dataspace> ds_cap,
                                 l4_uint64_t base, l4_umword_t offset,
                                 l4_umword_t size));

  /**
   * Get the dataspace with the L4virtio configuration page.
   *
   * \param config_ds  Capability for receiving the dataspace capability for
   *                   the shared L4-VIRTIO config data space.
   * \param ds_offset  Offset into the dataspace where the device configuration
   *                   structure starts.
   */
  L4_INLINE_RPC_OP(L4VIRTIO_OP_DEVICE_CONFIG, long, device_config,
                   (L4::Ipc::Out<L4::Cap<L4Re::Dataspace> > config_ds,
                    l4_addr_t *ds_offset));

  /**
   * Get the notification interrupt corresponding to the given index.
   *
   * \param      index  Index of the interrupt.
   * \param[out] irq    Triggerable for the given index.
   *
   * \retval L4_EOK     Success.
   * \retval L4_ENOSYS  IRQ notification not supported by device.
   * \retval <0         Other error.
   *
   * An index is only guaranteed to return an IRQ object when the index is
   * set in one of the device notify index fields. The device must return
   * the same interrupt for a given index as long as the index is in use.
   * If an index disappears as a result of a configuration change and then is
   * reused later, the interrupt is not guaranteed to be the same.
   *
   * Interrupts must always be rerequested after a device reset.
   */
  L4_INLINE_RPC_OP(L4VIRTIO_OP_GET_DEVICE_IRQ, long, device_notification_irq,
                   (unsigned index, L4::Ipc::Out<L4::Cap<L4::Triggerable> > irq));


  typedef L4::Typeid::Rpcs<set_status_t, config_queue_t, register_ds_t,
                           device_config_t, device_notification_irq_t>
    Rpcs;
};

}
