/** @file

  Driver for virtio-serial devices.

  Helper functions to manage virtio serial ports.
  Console ports will be registered as SerialIo UARTs.

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

**/

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

#include "VirtioSerial.h"

ACPI_HID_DEVICE_PATH  mAcpiSerialDevNode = {
  {
    ACPI_DEVICE_PATH,
    ACPI_DP,
    {
      (UINT8)(sizeof (ACPI_HID_DEVICE_PATH)),
      (UINT8)((sizeof (ACPI_HID_DEVICE_PATH)) >> 8)
    },
  },
  EISA_PNP_ID (0x0501),
  0
};

UART_DEVICE_PATH  mUartDevNode = {
  {
    MESSAGING_DEVICE_PATH,
    MSG_UART_DP,
    {
      (UINT8)(sizeof (UART_DEVICE_PATH)),
      (UINT8)((sizeof (UART_DEVICE_PATH)) >> 8)
    }
  },
  0,      // Reserved
  115200, // Speed
  8, 1, 1 // 8n1
};

STATIC
UINT16
PortRx (
  IN UINT32  PortId
  )
{
  ASSERT (PortId < MAX_PORTS);

  if (PortId >= 1) {
    return (UINT16)(VIRTIO_SERIAL_Q_RX_BASE + (PortId - 1) * 2);
  }

  return VIRTIO_SERIAL_Q_RX_PORT0;
}

STATIC
UINT16
PortTx (
  IN UINT32  PortId
  )
{
  ASSERT (PortId < MAX_PORTS);

  if (PortId >= 1) {
    return (UINT16)(VIRTIO_SERIAL_Q_TX_BASE + (PortId - 1) * 2);
  }

  return VIRTIO_SERIAL_Q_TX_PORT0;
}

STATIC
EFI_STATUS
EFIAPI
VirtioSerialIoReset (
  IN EFI_SERIAL_IO_PROTOCOL  *This
  )
{
  DEBUG ((DEBUG_VERBOSE, "%a:%d:\n", __func__, __LINE__));
  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
EFIAPI
VirtioSerialIoSetAttributes (
  IN EFI_SERIAL_IO_PROTOCOL  *This,
  IN UINT64                  BaudRate,
  IN UINT32                  ReceiveFifoDepth,
  IN UINT32                  Timeout,
  IN EFI_PARITY_TYPE         Parity,
  IN UINT8                   DataBits,
  IN EFI_STOP_BITS_TYPE      StopBits
  )
{
  DEBUG ((
    DEBUG_VERBOSE,
    "%a:%d: Rate %ld, Fifo %d, Bits %d\n",
    __func__,
    __LINE__,
    BaudRate,
    ReceiveFifoDepth,
    DataBits
    ));
  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
EFIAPI
VirtioSerialIoSetControl (
  IN EFI_SERIAL_IO_PROTOCOL  *This,
  IN UINT32                  Control
  )
{
  DEBUG ((DEBUG_INFO, "%a:%d: Control 0x%x\n", __func__, __LINE__, Control));
  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
EFIAPI
VirtioSerialIoGetControl (
  IN EFI_SERIAL_IO_PROTOCOL  *This,
  OUT UINT32                 *Control
  )
{
  DEBUG ((DEBUG_VERBOSE, "%a:%d: Control 0x%x\n", __func__, __LINE__, *Control));
  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
EFIAPI
VirtioSerialIoWrite (
  IN EFI_SERIAL_IO_PROTOCOL  *This,
  IN OUT UINTN               *BufferSize,
  IN VOID                    *Buffer
  )
{
  VIRTIO_SERIAL_IO_PROTOCOL  *SerialIo = (VIRTIO_SERIAL_IO_PROTOCOL *)This;
  VIRTIO_SERIAL_PORT         *Port     = SerialIo->Dev->Ports + SerialIo->PortId;
  UINT32                     Length;
  EFI_TPL                    OldTpl;

  if (!Port->DeviceOpen) {
    *BufferSize = 0;
    return EFI_SUCCESS;
  }

  VirtioSerialRingClearTx (SerialIo->Dev, PortTx (SerialIo->PortId));

  OldTpl = gBS->RaiseTPL (TPL_NOTIFY);
  if (SerialIo->WriteOffset &&
      (SerialIo->WriteOffset + *BufferSize > PORT_TX_BUFSIZE))
  {
    DEBUG ((DEBUG_VERBOSE, "%a:%d: WriteFlush %d\n", __func__, __LINE__, SerialIo->WriteOffset));
    VirtioSerialRingSendBuffer (
      SerialIo->Dev,
      PortTx (SerialIo->PortId),
      SerialIo->WriteBuffer,
      SerialIo->WriteOffset,
      TRUE
      );
    SerialIo->WriteOffset = 0;
  }

  Length = MIN ((UINT32)(*BufferSize), PORT_TX_BUFSIZE - SerialIo->WriteOffset);
  CopyMem (SerialIo->WriteBuffer + SerialIo->WriteOffset, Buffer, Length);
  SerialIo->WriteOffset += Length;
  *BufferSize            = Length;
  gBS->RestoreTPL (OldTpl);

  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
EFIAPI
VirtioSerialIoRead (
  IN EFI_SERIAL_IO_PROTOCOL  *This,
  IN OUT UINTN               *BufferSize,
  OUT VOID                   *Buffer
  )
{
  VIRTIO_SERIAL_IO_PROTOCOL  *SerialIo = (VIRTIO_SERIAL_IO_PROTOCOL *)This;
  VIRTIO_SERIAL_PORT         *Port     = SerialIo->Dev->Ports + SerialIo->PortId;
  BOOLEAN                    HasData;
  UINT32                     Length;
  EFI_TPL                    OldTpl;

  if (!Port->DeviceOpen) {
    goto NoData;
  }

  OldTpl = gBS->RaiseTPL (TPL_NOTIFY);
  if (SerialIo->WriteOffset) {
    DEBUG ((DEBUG_VERBOSE, "%a:%d: WriteFlush %d\n", __func__, __LINE__, SerialIo->WriteOffset));
    VirtioSerialRingSendBuffer (
      SerialIo->Dev,
      PortTx (SerialIo->PortId),
      SerialIo->WriteBuffer,
      SerialIo->WriteOffset,
      TRUE
      );
    SerialIo->WriteOffset = 0;
  }

  gBS->RestoreTPL (OldTpl);

  if (SerialIo->ReadOffset == SerialIo->ReadSize) {
    HasData = VirtioSerialRingGetBuffer (
                SerialIo->Dev,
                PortRx (SerialIo->PortId),
                &SerialIo->ReadBuffer,
                &SerialIo->ReadSize
                );
    if (!HasData) {
      goto NoData;
    }

    SerialIo->ReadOffset = 0;
  }

  if (SerialIo->ReadOffset < SerialIo->ReadSize) {
    Length = SerialIo->ReadSize - SerialIo->ReadOffset;
    if (Length > *BufferSize) {
      Length = (UINT32)(*BufferSize);
    }

    CopyMem (Buffer, SerialIo->ReadBuffer + SerialIo->ReadOffset, Length);
    SerialIo->ReadOffset += Length;
    *BufferSize           = Length;
    return EFI_SUCCESS;
  }

NoData:
  *BufferSize = 0;
  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
EFIAPI
VirtioSerialIoInit (
  IN OUT VIRTIO_SERIAL_DEV  *Dev,
  IN     UINT32             PortId
  )
{
  VIRTIO_SERIAL_PORT         *Port = Dev->Ports + PortId;
  VIRTIO_SERIAL_IO_PROTOCOL  *SerialIo;
  EFI_STATUS                 Status;

  SerialIo       = (VIRTIO_SERIAL_IO_PROTOCOL *)AllocateZeroPool (sizeof *SerialIo);
  Port->SerialIo = SerialIo;

  SerialIo->SerialIo.Revision      = EFI_SERIAL_IO_PROTOCOL_REVISION;
  SerialIo->SerialIo.Reset         = VirtioSerialIoReset;
  SerialIo->SerialIo.SetAttributes = VirtioSerialIoSetAttributes;
  SerialIo->SerialIo.SetControl    = VirtioSerialIoSetControl;
  SerialIo->SerialIo.GetControl    = VirtioSerialIoGetControl;
  SerialIo->SerialIo.Write         = VirtioSerialIoWrite;
  SerialIo->SerialIo.Read          = VirtioSerialIoRead;
  SerialIo->SerialIo.Mode          = &SerialIo->SerialIoMode;
  SerialIo->Dev                    = Dev;
  SerialIo->PortId                 = PortId;

  SerialIo->DevicePath   = DuplicateDevicePath (Dev->DevicePath);
  mAcpiSerialDevNode.UID = PortId;
  SerialIo->DevicePath   = AppendDevicePathNode (
                             SerialIo->DevicePath,
                             (EFI_DEVICE_PATH_PROTOCOL *)&mAcpiSerialDevNode
                             );
  SerialIo->DevicePath = AppendDevicePathNode (
                           SerialIo->DevicePath,
                           (EFI_DEVICE_PATH_PROTOCOL *)&mUartDevNode
                           );

  LogDevicePath (DEBUG_INFO, __func__, L"UART", SerialIo->DevicePath);

  Status = gBS->InstallMultipleProtocolInterfaces (
                  &SerialIo->DeviceHandle,
                  &gEfiDevicePathProtocolGuid,
                  SerialIo->DevicePath,
                  &gEfiSerialIoProtocolGuid,
                  &SerialIo->SerialIo,
                  NULL
                  );
  if (EFI_ERROR (Status)) {
    DEBUG ((DEBUG_INFO, "%a:%d: ERROR: %r\n", __func__, __LINE__, Status));
    goto FreeSerialIo;
  }

  Status = gBS->OpenProtocol (
                  Dev->DeviceHandle,
                  &gVirtioDeviceProtocolGuid,
                  (VOID **)&Dev->VirtIo,
                  Dev->DriverBindingHandle,
                  SerialIo->DeviceHandle,
                  EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER
                  );
  if (EFI_ERROR (Status)) {
    DEBUG ((DEBUG_INFO, "%a:%d: ERROR: %r\n", __func__, __LINE__, Status));
    goto UninstallProtocol;
  }

  return EFI_SUCCESS;

UninstallProtocol:
  gBS->UninstallMultipleProtocolInterfaces (
         SerialIo->DeviceHandle,
         &gEfiDevicePathProtocolGuid,
         SerialIo->DevicePath,
         &gEfiSerialIoProtocolGuid,
         &SerialIo->SerialIo,
         NULL
         );

FreeSerialIo:
  FreePool (Port->SerialIo);
  Port->SerialIo = NULL;
  return Status;
}

STATIC
VOID
EFIAPI
VirtioSerialIoUninit (
  VIRTIO_SERIAL_IO_PROTOCOL  *SerialIo
  )
{
  VIRTIO_SERIAL_DEV   *Dev  = SerialIo->Dev;
  VIRTIO_SERIAL_PORT  *Port = Dev->Ports + SerialIo->PortId;

  DEBUG ((DEBUG_INFO, "%a:%d: %s\n", __func__, __LINE__, Port->Name));

  gBS->CloseProtocol (
         Dev->DeviceHandle,
         &gVirtioDeviceProtocolGuid,
         Dev->DriverBindingHandle,
         SerialIo->DeviceHandle
         );

  gBS->UninstallMultipleProtocolInterfaces (
         SerialIo->DeviceHandle,
         &gEfiDevicePathProtocolGuid,
         SerialIo->DevicePath,
         &gEfiSerialIoProtocolGuid,
         &SerialIo->SerialIo,
         NULL
         );

  FreePool (SerialIo);
  Port->SerialIo = NULL;
}

EFI_STATUS
EFIAPI
VirtioSerialPortAdd (
  IN OUT VIRTIO_SERIAL_DEV  *Dev,
  IN     UINT32             PortId
  )
{
  VIRTIO_SERIAL_PORT  *Port = Dev->Ports + PortId;
  EFI_STATUS          Status;

  if (Port->Ready) {
    return EFI_SUCCESS;
  }

  Status = VirtioSerialInitRing (Dev, PortRx (PortId), PORT_RX_BUFSIZE);
  if (EFI_ERROR (Status)) {
    goto Failed;
  }

  Status = VirtioSerialInitRing (Dev, PortTx (PortId), PORT_TX_BUFSIZE);
  if (EFI_ERROR (Status)) {
    goto Failed;
  }

  UnicodeSPrint (Port->Name, sizeof (Port->Name), L"Port #%d", PortId);
  VirtioSerialRingFillRx (Dev, PortRx (PortId));
  Port->Ready = TRUE;

  return EFI_SUCCESS;

Failed:
  VirtioSerialUninitRing (Dev, PortRx (PortId));
  return Status;
}

VOID
EFIAPI
VirtioSerialPortSetConsole (
  IN OUT VIRTIO_SERIAL_DEV  *Dev,
  IN     UINT32             PortId
  )
{
  VIRTIO_SERIAL_PORT  *Port = Dev->Ports + PortId;

  Port->Console = TRUE;
  UnicodeSPrint (Port->Name, sizeof (Port->Name), L"Console #%d", PortId);
  VirtioSerialIoInit (Dev, PortId);
}

VOID
EFIAPI
VirtioSerialPortSetName (
  IN OUT VIRTIO_SERIAL_DEV  *Dev,
  IN     UINT32             PortId,
  IN     UINT8              *Name
  )
{
  VIRTIO_SERIAL_PORT  *Port = Dev->Ports + PortId;

  DEBUG ((DEBUG_INFO, "%a:%d: \"%a\"\n", __func__, __LINE__, Name));
  UnicodeSPrint (Port->Name, sizeof (Port->Name), L"NamedPort #%d (%a)", PortId, Name);
}

VOID
EFIAPI
VirtioSerialPortSetDeviceOpen (
  IN OUT VIRTIO_SERIAL_DEV  *Dev,
  IN     UINT32             PortId,
  IN     UINT16             Value
  )
{
  VIRTIO_SERIAL_PORT  *Port = Dev->Ports + PortId;

  Port->DeviceOpen = (BOOLEAN)Value;
  if (Port->DeviceOpen) {
    VirtioSerialTxControl (Dev, PortId, VIRTIO_SERIAL_PORT_OPEN, 1);
  }
}

VOID
EFIAPI
VirtioSerialPortRemove (
  IN OUT VIRTIO_SERIAL_DEV  *Dev,
  IN     UINT32             PortId
  )
{
  VIRTIO_SERIAL_PORT  *Port = Dev->Ports + PortId;

  if (!Port->Ready) {
    return;
  }

  if (Port->SerialIo) {
    VirtioSerialIoUninit (Port->SerialIo);
    Port->SerialIo = NULL;
  }

  VirtioSerialUninitRing (Dev, PortRx (PortId));
  VirtioSerialUninitRing (Dev, PortTx (PortId));
  Port->Ready = FALSE;
}
