/** @file

  Driver for virtio-serial devices.

  Helper functions to manage virtio rings.

  SPDX-License-Identifier: BSD-2-Clause-Patent

**/

#include <Library/BaseMemoryLib.h>
#include <Library/DebugLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiLib.h>
#include <Library/VirtioLib.h>

#include "VirtioSerial.h"

STATIC
VOID *
BufferPtr (
  IN VIRTIO_SERIAL_RING  *Ring,
  IN UINT32              BufferNr
  )
{
  return Ring->Buffers + Ring->BufferSize * BufferNr;
}

STATIC
EFI_PHYSICAL_ADDRESS
BufferAddr (
  IN VIRTIO_SERIAL_RING  *Ring,
  IN UINT32              BufferNr
  )
{
  return Ring->DeviceAddress + Ring->BufferSize * BufferNr;
}

STATIC
UINT32
BufferNext (
  IN VIRTIO_SERIAL_RING  *Ring
  )
{
  return Ring->Indices.NextDescIdx % Ring->Ring.QueueSize;
}

EFI_STATUS
EFIAPI
VirtioSerialInitRing (
  IN OUT VIRTIO_SERIAL_DEV  *Dev,
  IN     UINT16             Index,
  IN     UINT32             BufferSize
  )
{
  VIRTIO_SERIAL_RING  *Ring = Dev->Rings + Index;
  EFI_STATUS          Status;
  UINT16              QueueSize;
  UINT64              RingBaseShift;

  //
  // step 4b -- allocate request virtqueue
  //
  Status = Dev->VirtIo->SetQueueSel (Dev->VirtIo, Index);
  if (EFI_ERROR (Status)) {
    goto Failed;
  }

  Status = Dev->VirtIo->GetQueueNumMax (Dev->VirtIo, &QueueSize);
  if (EFI_ERROR (Status)) {
    goto Failed;
  }

  //
  // VirtioSerial uses one descriptor
  //
  if (QueueSize < 1) {
    Status = EFI_UNSUPPORTED;
    goto Failed;
  }

  Status = VirtioRingInit (Dev->VirtIo, QueueSize, &Ring->Ring);
  if (EFI_ERROR (Status)) {
    goto Failed;
  }

  //
  // If anything fails from here on, we must release the ring resources.
  //
  Status = VirtioRingMap (
             Dev->VirtIo,
             &Ring->Ring,
             &RingBaseShift,
             &Ring->RingMap
             );
  if (EFI_ERROR (Status)) {
    goto ReleaseQueue;
  }

  //
  // Additional steps for MMIO: align the queue appropriately, and set the
  // size. If anything fails from here on, we must unmap the ring resources.
  //
  Status = Dev->VirtIo->SetQueueNum (Dev->VirtIo, QueueSize);
  if (EFI_ERROR (Status)) {
    goto UnmapQueue;
  }

  Status = Dev->VirtIo->SetQueueAlign (Dev->VirtIo, EFI_PAGE_SIZE);
  if (EFI_ERROR (Status)) {
    goto UnmapQueue;
  }

  //
  // step 4c -- Report GPFN (guest-physical frame number) of queue.
  //
  Status = Dev->VirtIo->SetQueueAddress (
                          Dev->VirtIo,
                          &Ring->Ring,
                          RingBaseShift
                          );
  if (EFI_ERROR (Status)) {
    goto UnmapQueue;
  }

  Ring->BufferCount = QueueSize;
  Ring->BufferSize  = BufferSize;
  Ring->BufferPages = EFI_SIZE_TO_PAGES (Ring->BufferCount * Ring->BufferSize);

  Status = Dev->VirtIo->AllocateSharedPages (Dev->VirtIo, Ring->BufferPages, (VOID **)&Ring->Buffers);
  if (EFI_ERROR (Status)) {
    goto UnmapQueue;
  }

  Status = VirtioMapAllBytesInSharedBuffer (
             Dev->VirtIo,
             VirtioOperationBusMasterCommonBuffer,
             Ring->Buffers,
             EFI_PAGES_TO_SIZE (Ring->BufferPages),
             &Ring->DeviceAddress,
             &Ring->BufferMap
             );
  if (EFI_ERROR (Status)) {
    goto ReleasePages;
  }

  VirtioPrepare (&Ring->Ring, &Ring->Indices);
  Ring->Ready = TRUE;

  return EFI_SUCCESS;

ReleasePages:
  Dev->VirtIo->FreeSharedPages (
                 Dev->VirtIo,
                 Ring->BufferPages,
                 Ring->Buffers
                 );
  Ring->Buffers = NULL;

UnmapQueue:
  Dev->VirtIo->UnmapSharedBuffer (Dev->VirtIo, Ring->RingMap);
  Ring->RingMap = NULL;

ReleaseQueue:
  VirtioRingUninit (Dev->VirtIo, &Ring->Ring);

Failed:
  return Status;
}

VOID
EFIAPI
VirtioSerialUninitRing (
  IN OUT VIRTIO_SERIAL_DEV  *Dev,
  IN     UINT16             Index
  )
{
  VIRTIO_SERIAL_RING  *Ring = Dev->Rings + Index;

  if (Ring->BufferMap) {
    Dev->VirtIo->UnmapSharedBuffer (Dev->VirtIo, Ring->BufferMap);
    Ring->BufferMap = NULL;
  }

  if (Ring->Buffers) {
    Dev->VirtIo->FreeSharedPages (
                   Dev->VirtIo,
                   Ring->BufferPages,
                   Ring->Buffers
                   );
    Ring->Buffers = NULL;
  }

  if (!Ring->RingMap) {
    Dev->VirtIo->UnmapSharedBuffer (Dev->VirtIo, Ring->RingMap);
    Ring->RingMap = NULL;
  }

  if (Ring->Ring.Base) {
    VirtioRingUninit (Dev->VirtIo, &Ring->Ring);
  }

  ZeroMem (Ring, sizeof (*Ring));
}

VOID
EFIAPI
VirtioSerialRingFillRx (
  IN OUT VIRTIO_SERIAL_DEV  *Dev,
  IN     UINT16             Index
  )
{
  VIRTIO_SERIAL_RING  *Ring = Dev->Rings + Index;
  UINT32              BufferNr;

  for (BufferNr = 0; BufferNr < Ring->BufferCount; BufferNr++) {
    VirtioSerialRingSendBuffer (Dev, Index, NULL, Ring->BufferSize, FALSE);
  }

  Dev->VirtIo->SetQueueNotify (Dev->VirtIo, Index);
}

VOID
EFIAPI
VirtioSerialRingClearTx (
  IN OUT VIRTIO_SERIAL_DEV  *Dev,
  IN     UINT16             Index
  )
{
  while (VirtioSerialRingGetBuffer (Dev, Index, NULL, NULL)) {
    /* nothing */ }
}

EFI_STATUS
EFIAPI
VirtioSerialRingSendBuffer (
  IN OUT VIRTIO_SERIAL_DEV  *Dev,
  IN     UINT16             Index,
  IN     VOID               *Data,
  IN     UINT32             DataSize,
  IN     BOOLEAN            Notify
  )
{
  VIRTIO_SERIAL_RING  *Ring    = Dev->Rings + Index;
  UINT32              BufferNr = BufferNext (Ring);
  UINT16              Idx      = *Ring->Ring.Avail.Idx;
  UINT16              Flags    = 0;

  ASSERT (DataSize <= Ring->BufferSize);

  if (Data) {
    /* driver -> device */
    CopyMem (BufferPtr (Ring, BufferNr), Data, DataSize);
  } else {
    /* device -> driver */
    Flags |= VRING_DESC_F_WRITE;
  }

  VirtioAppendDesc (
    &Ring->Ring,
    BufferAddr (Ring, BufferNr),
    DataSize,
    Flags,
    &Ring->Indices
    );

  Ring->Ring.Avail.Ring[Idx % Ring->Ring.QueueSize] =
    Ring->Indices.HeadDescIdx % Ring->Ring.QueueSize;
  Ring->Indices.HeadDescIdx = Ring->Indices.NextDescIdx;
  Idx++;

  MemoryFence ();
  *Ring->Ring.Avail.Idx = Idx;
  MemoryFence ();

  if (Notify) {
    Dev->VirtIo->SetQueueNotify (Dev->VirtIo, Index);
  }

  return EFI_SUCCESS;
}

BOOLEAN
EFIAPI
VirtioSerialRingHasBuffer (
  IN OUT VIRTIO_SERIAL_DEV  *Dev,
  IN     UINT16             Index
  )
{
  VIRTIO_SERIAL_RING  *Ring   = Dev->Rings + Index;
  UINT16              UsedIdx = *Ring->Ring.Used.Idx;

  if (!Ring->Ready) {
    return FALSE;
  }

  if (Ring->LastUsedIdx == UsedIdx) {
    return FALSE;
  }

  return TRUE;
}

BOOLEAN
EFIAPI
VirtioSerialRingGetBuffer (
  IN OUT VIRTIO_SERIAL_DEV  *Dev,
  IN     UINT16             Index,
  OUT    VOID               *Data,
  OUT    UINT32             *DataSize
  )
{
  VIRTIO_SERIAL_RING        *Ring   = Dev->Rings + Index;
  UINT16                    UsedIdx = *Ring->Ring.Used.Idx;
  volatile VRING_USED_ELEM  *UsedElem;

  if (!Ring->Ready) {
    return FALSE;
  }

  if (Ring->LastUsedIdx == UsedIdx) {
    return FALSE;
  }

  UsedElem = Ring->Ring.Used.UsedElem + (Ring->LastUsedIdx % Ring->Ring.QueueSize);

  if (UsedElem->Len > Ring->BufferSize) {
    DEBUG ((DEBUG_ERROR, "%a:%d: %d: invalid length\n", __func__, __LINE__, Index));
    UsedElem->Len = 0;
  }

  if (Data && DataSize) {
    CopyMem (Data, BufferPtr (Ring, UsedElem->Id), UsedElem->Len);
    *DataSize = UsedElem->Len;
  }

  if (Index % 2 == 0) {
    /* RX - re-queue buffer */
    VirtioSerialRingSendBuffer (Dev, Index, NULL, Ring->BufferSize, FALSE);
  }

  Ring->LastUsedIdx++;
  return TRUE;
}
