/** @file

  This driver produces Extended SCSI Pass Thru Protocol instances for
  pvscsi devices.

  Copyright (C) 2020, Oracle and/or its affiliates.

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

**/

#include <IndustryStandard/Pci.h>
#include <IndustryStandard/PvScsi.h>
#include <Library/BaseLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiLib.h>
#include <Protocol/PciIo.h>
#include <Protocol/PciRootBridgeIo.h>
#include <Uefi/UefiSpec.h>

#include "PvScsi.h"

//
// Higher versions will be used before lower, 0x10-0xffffffef is the version
// range for IHV (Indie Hardware Vendors)
//
#define PVSCSI_BINDING_VERSION  0x10

//
// Ext SCSI Pass Thru utilities
//

/**
  Reads a 32-bit value into BAR0 using MMIO
**/
STATIC
EFI_STATUS
PvScsiMmioRead32 (
  IN CONST PVSCSI_DEV  *Dev,
  IN UINT64            Offset,
  OUT UINT32           *Value
  )
{
  return Dev->PciIo->Mem.Read (
                           Dev->PciIo,
                           EfiPciIoWidthUint32,
                           PCI_BAR_IDX0,
                           Offset,
                           1,   // Count
                           Value
                           );
}

/**
  Writes a 32-bit value into BAR0 using MMIO
**/
STATIC
EFI_STATUS
PvScsiMmioWrite32 (
  IN CONST PVSCSI_DEV  *Dev,
  IN UINT64            Offset,
  IN UINT32            Value
  )
{
  return Dev->PciIo->Mem.Write (
                           Dev->PciIo,
                           EfiPciIoWidthUint32,
                           PCI_BAR_IDX0,
                           Offset,
                           1,   // Count
                           &Value
                           );
}

/**
  Writes multiple words of data into BAR0 using MMIO
**/
STATIC
EFI_STATUS
PvScsiMmioWrite32Multiple (
  IN CONST PVSCSI_DEV  *Dev,
  IN UINT64            Offset,
  IN UINTN             Count,
  IN UINT32            *Words
  )
{
  return Dev->PciIo->Mem.Write (
                           Dev->PciIo,
                           EfiPciIoWidthFifoUint32,
                           PCI_BAR_IDX0,
                           Offset,
                           Count,
                           Words
                           );
}

/**
  Send a PVSCSI command to device.

  @param[in] Dev                    The pvscsi host device.
  @param[in] Cmd                    The command to send to device.
  @param[in] OPTIONAL DescWords     An optional command descriptor (If command
                                    have a descriptor). The descriptor is
                                    provided as an array of UINT32 words and
                                    is must be 32-bit aligned.
  @param[in] DescWordsCount         The number of words in command descriptor.
                                    Caller must specify here 0 if DescWords
                                    is not supplied (It is optional). In that
                                    case, DescWords is ignored.

  @return   Status codes returned by Dev->PciIo->Mem.Write().

**/
STATIC
EFI_STATUS
PvScsiWriteCmdDesc (
  IN CONST PVSCSI_DEV  *Dev,
  IN UINT32            Cmd,
  IN UINT32            *DescWords      OPTIONAL,
  IN UINTN             DescWordsCount
  )
{
  EFI_STATUS  Status;

  if (DescWordsCount > PVSCSI_MAX_CMD_DATA_WORDS) {
    return EFI_INVALID_PARAMETER;
  }

  Status = PvScsiMmioWrite32 (Dev, PvScsiRegOffsetCommand, Cmd);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  if (DescWordsCount > 0) {
    return PvScsiMmioWrite32Multiple (
             Dev,
             PvScsiRegOffsetCommandData,
             DescWordsCount,
             DescWords
             );
  }

  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
PvScsiResetAdapter (
  IN CONST PVSCSI_DEV  *Dev
  )
{
  return PvScsiWriteCmdDesc (Dev, PvScsiCmdAdapterReset, NULL, 0);
}

/**
  Returns if PVSCSI request ring is full
**/
STATIC
BOOLEAN
PvScsiIsReqRingFull (
  IN CONST PVSCSI_DEV  *Dev
  )
{
  PVSCSI_RINGS_STATE  *RingsState;
  UINT32              ReqNumEntries;

  RingsState    = Dev->RingDesc.RingState;
  ReqNumEntries = 1U << RingsState->ReqNumEntriesLog2;
  return (RingsState->ReqProdIdx - RingsState->CmpConsIdx) >= ReqNumEntries;
}

/**
  Returns pointer to current request descriptor to produce
**/
STATIC
PVSCSI_RING_REQ_DESC *
PvScsiGetCurrentRequest (
  IN CONST PVSCSI_DEV  *Dev
  )
{
  PVSCSI_RINGS_STATE  *RingState;
  UINT32              ReqNumEntries;

  RingState     = Dev->RingDesc.RingState;
  ReqNumEntries = 1U << RingState->ReqNumEntriesLog2;
  return Dev->RingDesc.RingReqs +
         (RingState->ReqProdIdx & (ReqNumEntries - 1));
}

/**
  Returns pointer to current completion descriptor to consume
**/
STATIC
PVSCSI_RING_CMP_DESC *
PvScsiGetCurrentResponse (
  IN CONST PVSCSI_DEV  *Dev
  )
{
  PVSCSI_RINGS_STATE  *RingState;
  UINT32              CmpNumEntries;

  RingState     = Dev->RingDesc.RingState;
  CmpNumEntries = 1U << RingState->CmpNumEntriesLog2;
  return Dev->RingDesc.RingCmps +
         (RingState->CmpConsIdx & (CmpNumEntries - 1));
}

/**
  Wait for device to signal completion of submitted requests
**/
STATIC
EFI_STATUS
PvScsiWaitForRequestCompletion (
  IN CONST PVSCSI_DEV  *Dev
  )
{
  EFI_STATUS  Status;
  UINT32      IntrStatus;

  //
  // Note: We don't yet support Timeout according to
  // EFI_EXT_SCSI_PASS_THRU_SCSI_REQUEST_PACKET.Timeout.
  //
  // This is consistent with some other Scsi PassThru drivers
  // such as VirtioScsi.
  //
  for ( ; ;) {
    Status = PvScsiMmioRead32 (Dev, PvScsiRegOffsetIntrStatus, &IntrStatus);
    if (EFI_ERROR (Status)) {
      return Status;
    }

    //
    // PVSCSI_INTR_CMPL_MASK is set if device completed submitted requests
    //
    if ((IntrStatus & PVSCSI_INTR_CMPL_MASK) != 0) {
      break;
    }

    gBS->Stall (Dev->WaitForCmpStallInUsecs);
  }

  //
  // Acknowledge PVSCSI_INTR_CMPL_MASK in device interrupt-status register
  //
  return PvScsiMmioWrite32 (
           Dev,
           PvScsiRegOffsetIntrStatus,
           PVSCSI_INTR_CMPL_MASK
           );
}

/**
  Create a fake host adapter error
**/
STATIC
EFI_STATUS
ReportHostAdapterError (
  OUT EFI_EXT_SCSI_PASS_THRU_SCSI_REQUEST_PACKET  *Packet
  )
{
  Packet->InTransferLength  = 0;
  Packet->OutTransferLength = 0;
  Packet->SenseDataLength   = 0;
  Packet->HostAdapterStatus = EFI_EXT_SCSI_STATUS_HOST_ADAPTER_OTHER;
  Packet->TargetStatus      = EFI_EXT_SCSI_STATUS_TARGET_GOOD;
  return EFI_DEVICE_ERROR;
}

/**
  Create a fake host adapter overrun error
**/
STATIC
EFI_STATUS
ReportHostAdapterOverrunError (
  OUT EFI_EXT_SCSI_PASS_THRU_SCSI_REQUEST_PACKET  *Packet
  )
{
  Packet->SenseDataLength   = 0;
  Packet->HostAdapterStatus =
    EFI_EXT_SCSI_STATUS_HOST_ADAPTER_DATA_OVERRUN_UNDERRUN;
  Packet->TargetStatus = EFI_EXT_SCSI_STATUS_TARGET_GOOD;
  return EFI_BAD_BUFFER_SIZE;
}

/**
  Populate a PVSCSI request descriptor from the Extended SCSI Pass Thru
  Protocol packet.
**/
STATIC
EFI_STATUS
PopulateRequest (
  IN CONST PVSCSI_DEV                                *Dev,
  IN UINT8                                           *Target,
  IN UINT64                                          Lun,
  IN OUT EFI_EXT_SCSI_PASS_THRU_SCSI_REQUEST_PACKET  *Packet,
  OUT PVSCSI_RING_REQ_DESC                           *Request
  )
{
  UINT8  TargetValue;

  //
  // We only use first byte of target identifer
  //
  TargetValue = *Target;

  //
  // Check for unsupported requests
  //
  if (
      //
      // Bidirectional transfer was requested
      //
      ((Packet->InTransferLength > 0) && (Packet->OutTransferLength > 0)) ||
      (Packet->DataDirection == EFI_EXT_SCSI_DATA_DIRECTION_BIDIRECTIONAL) ||
      //
      // Command Descriptor Block bigger than this constant should be considered
      // out-of-band. We currently don't support these CDBs.
      //
      (Packet->CdbLength > PVSCSI_CDB_MAX_SIZE)
      )
  {
    //
    // This error code doesn't require updates to the Packet output fields
    //
    return EFI_UNSUPPORTED;
  }

  //
  // Check for invalid parameters
  //
  if (
      //
      // Addressed invalid device
      //
      (TargetValue > Dev->MaxTarget) || (Lun > Dev->MaxLun) ||
      //
      // Invalid direction (there doesn't seem to be a macro for the "no data
      // transferred" "direction", eg. for TEST UNIT READY)
      //
      (Packet->DataDirection > EFI_EXT_SCSI_DATA_DIRECTION_BIDIRECTIONAL) ||
      //
      // Trying to receive, but destination pointer is NULL, or contradicting
      // transfer direction
      //
      ((Packet->InTransferLength > 0) &&
       ((Packet->InDataBuffer == NULL) ||
        (Packet->DataDirection == EFI_EXT_SCSI_DATA_DIRECTION_WRITE)
       )
      ) ||
      //
      // Trying to send, but source pointer is NULL, or contradicting
      // transfer direction
      //
      ((Packet->OutTransferLength > 0) &&
       ((Packet->OutDataBuffer == NULL) ||
        (Packet->DataDirection == EFI_EXT_SCSI_DATA_DIRECTION_READ)
       )
      )
      )
  {
    //
    // This error code doesn't require updates to the Packet output fields
    //
    return EFI_INVALID_PARAMETER;
  }

  //
  // Check for input/output buffer too large for DMA communication buffer
  //
  if (Packet->InTransferLength > sizeof (Dev->DmaBuf->Data)) {
    Packet->InTransferLength = sizeof (Dev->DmaBuf->Data);
    return ReportHostAdapterOverrunError (Packet);
  }

  if (Packet->OutTransferLength > sizeof (Dev->DmaBuf->Data)) {
    Packet->OutTransferLength = sizeof (Dev->DmaBuf->Data);
    return ReportHostAdapterOverrunError (Packet);
  }

  //
  // Encode PVSCSI request
  //
  ZeroMem (Request, sizeof (*Request));

  Request->Bus    = 0;
  Request->Target = TargetValue;
  //
  // This cast is safe as PVSCSI_DEV.MaxLun is defined as UINT8
  //
  Request->Lun[1]   = (UINT8)Lun;
  Request->SenseLen = Packet->SenseDataLength;
  //
  // DMA communication buffer SenseData overflow is not possible
  // due to Packet->SenseDataLength defined as UINT8
  //
  Request->SenseAddr = PVSCSI_DMA_BUF_DEV_ADDR (Dev, SenseData);
  Request->CdbLen    = Packet->CdbLength;
  CopyMem (Request->Cdb, Packet->Cdb, Packet->CdbLength);
  Request->VcpuHint = 0;
  Request->Tag      = PVSCSI_SIMPLE_QUEUE_TAG;
  if (Packet->DataDirection == EFI_EXT_SCSI_DATA_DIRECTION_READ) {
    Request->Flags   = PVSCSI_FLAG_CMD_DIR_TOHOST;
    Request->DataLen = Packet->InTransferLength;
  } else {
    Request->Flags   = PVSCSI_FLAG_CMD_DIR_TODEVICE;
    Request->DataLen = Packet->OutTransferLength;
    CopyMem (
      Dev->DmaBuf->Data,
      Packet->OutDataBuffer,
      Packet->OutTransferLength
      );
  }

  Request->DataAddr = PVSCSI_DMA_BUF_DEV_ADDR (Dev, Data);

  return EFI_SUCCESS;
}

/**
  Handle the PVSCSI device response:
  - Copy returned data from DMA communication buffer.
  - Update fields in Extended SCSI Pass Thru Protocol packet as required.
  - Translate response code to EFI status code and host adapter status.
**/
STATIC
EFI_STATUS
HandleResponse (
  IN PVSCSI_DEV                                      *Dev,
  IN OUT EFI_EXT_SCSI_PASS_THRU_SCSI_REQUEST_PACKET  *Packet,
  IN CONST PVSCSI_RING_CMP_DESC                      *Response
  )
{
  //
  // Fix SenseDataLength to amount of data returned
  //
  if (Packet->SenseDataLength > Response->SenseLen) {
    Packet->SenseDataLength = (UINT8)Response->SenseLen;
  }

  //
  // Copy sense data from DMA communication buffer
  //
  CopyMem (
    Packet->SenseData,
    Dev->DmaBuf->SenseData,
    Packet->SenseDataLength
    );

  //
  // Copy device output from DMA communication buffer
  //
  if (Packet->DataDirection == EFI_EXT_SCSI_DATA_DIRECTION_READ) {
    CopyMem (Packet->InDataBuffer, Dev->DmaBuf->Data, Packet->InTransferLength);
  }

  //
  // Report target status
  // (Strangely, PVSCSI interface defines Response->ScsiStatus as UINT16.
  // But it should de-facto always have a value that fits UINT8. To avoid
  // unexpected behavior, verify value is in UINT8 bounds before casting)
  //
  ASSERT (Response->ScsiStatus <= MAX_UINT8);
  Packet->TargetStatus = (UINT8)Response->ScsiStatus;

  //
  // Host adapter status and function return value depend on
  // device response's host status
  //
  switch (Response->HostStatus) {
    case PvScsiBtStatSuccess:
    case PvScsiBtStatLinkedCommandCompleted:
    case PvScsiBtStatLinkedCommandCompletedWithFlag:
      Packet->HostAdapterStatus = EFI_EXT_SCSI_STATUS_HOST_ADAPTER_OK;
      return EFI_SUCCESS;

    case PvScsiBtStatDataUnderrun:
      //
      // Report transferred amount in underrun
      //
      if (Packet->DataDirection == EFI_EXT_SCSI_DATA_DIRECTION_READ) {
        Packet->InTransferLength = (UINT32)Response->DataLen;
      } else {
        Packet->OutTransferLength = (UINT32)Response->DataLen;
      }

      Packet->HostAdapterStatus =
        EFI_EXT_SCSI_STATUS_HOST_ADAPTER_DATA_OVERRUN_UNDERRUN;
      return EFI_SUCCESS;

    case PvScsiBtStatDatarun:
      Packet->HostAdapterStatus =
        EFI_EXT_SCSI_STATUS_HOST_ADAPTER_DATA_OVERRUN_UNDERRUN;
      return EFI_SUCCESS;

    case PvScsiBtStatSelTimeout:
      Packet->HostAdapterStatus =
        EFI_EXT_SCSI_STATUS_HOST_ADAPTER_SELECTION_TIMEOUT;
      return EFI_TIMEOUT;

    case PvScsiBtStatBusFree:
      Packet->HostAdapterStatus = EFI_EXT_SCSI_STATUS_HOST_ADAPTER_BUS_FREE;
      break;

    case PvScsiBtStatInvPhase:
      Packet->HostAdapterStatus = EFI_EXT_SCSI_STATUS_HOST_ADAPTER_PHASE_ERROR;
      break;

    case PvScsiBtStatSensFailed:
      Packet->HostAdapterStatus =
        EFI_EXT_SCSI_STATUS_HOST_ADAPTER_REQUEST_SENSE_FAILED;
      break;

    case PvScsiBtStatTagReject:
    case PvScsiBtStatBadMsg:
      Packet->HostAdapterStatus =
        EFI_EXT_SCSI_STATUS_HOST_ADAPTER_MESSAGE_REJECT;
      break;

    case PvScsiBtStatBusReset:
      Packet->HostAdapterStatus = EFI_EXT_SCSI_STATUS_HOST_ADAPTER_BUS_RESET;
      break;

    case PvScsiBtStatHaTimeout:
      Packet->HostAdapterStatus = EFI_EXT_SCSI_STATUS_HOST_ADAPTER_TIMEOUT;
      return EFI_TIMEOUT;

    case PvScsiBtStatScsiParity:
      Packet->HostAdapterStatus = EFI_EXT_SCSI_STATUS_HOST_ADAPTER_PARITY_ERROR;
      break;

    default:
      Packet->HostAdapterStatus = EFI_EXT_SCSI_STATUS_HOST_ADAPTER_OTHER;
      break;
  }

  return EFI_DEVICE_ERROR;
}

/**
  Check if Target argument to EXT_SCSI_PASS_THRU.GetNextTarget() and
  EXT_SCSI_PASS_THRU.GetNextTargetLun() is initialized
**/
STATIC
BOOLEAN
IsTargetInitialized (
  IN UINT8  *Target
  )
{
  UINTN  Idx;

  for (Idx = 0; Idx < TARGET_MAX_BYTES; ++Idx) {
    if (Target[Idx] != 0xFF) {
      return TRUE;
    }
  }

  return FALSE;
}

//
// Ext SCSI Pass Thru
//

STATIC
EFI_STATUS
EFIAPI
PvScsiPassThru (
  IN EFI_EXT_SCSI_PASS_THRU_PROTOCOL                 *This,
  IN UINT8                                           *Target,
  IN UINT64                                          Lun,
  IN OUT EFI_EXT_SCSI_PASS_THRU_SCSI_REQUEST_PACKET  *Packet,
  IN EFI_EVENT                                       Event    OPTIONAL
  )
{
  PVSCSI_DEV            *Dev;
  EFI_STATUS            Status;
  PVSCSI_RING_REQ_DESC  *Request;
  PVSCSI_RING_CMP_DESC  *Response;

  Dev = PVSCSI_FROM_PASS_THRU (This);

  if (PvScsiIsReqRingFull (Dev)) {
    return EFI_NOT_READY;
  }

  Request = PvScsiGetCurrentRequest (Dev);

  Status = PopulateRequest (Dev, Target, Lun, Packet, Request);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  //
  // Writes to Request must be globally visible before making request
  // available to device
  //
  MemoryFence ();
  Dev->RingDesc.RingState->ReqProdIdx++;

  Status = PvScsiMmioWrite32 (Dev, PvScsiRegOffsetKickRwIo, 0);
  if (EFI_ERROR (Status)) {
    //
    // If kicking the host fails, we must fake a host adapter error.
    // EFI_NOT_READY would save us the effort, but it would also suggest that
    // the caller retry.
    //
    return ReportHostAdapterError (Packet);
  }

  Status = PvScsiWaitForRequestCompletion (Dev);
  if (EFI_ERROR (Status)) {
    //
    // If waiting for request completion fails, we must fake a host adapter
    // error. EFI_NOT_READY would save us the effort, but it would also suggest
    // that the caller retry.
    //
    return ReportHostAdapterError (Packet);
  }

  Response = PvScsiGetCurrentResponse (Dev);
  Status   = HandleResponse (Dev, Packet, Response);

  //
  // Reads from response must complete before releasing completion entry
  // to device
  //
  MemoryFence ();
  Dev->RingDesc.RingState->CmpConsIdx++;

  return Status;
}

STATIC
EFI_STATUS
EFIAPI
PvScsiGetNextTargetLun (
  IN EFI_EXT_SCSI_PASS_THRU_PROTOCOL  *This,
  IN OUT UINT8                        **Target,
  IN OUT UINT64                       *Lun
  )
{
  UINT8       *TargetPtr;
  UINT8       LastTarget;
  PVSCSI_DEV  *Dev;

  if (Target == NULL) {
    return EFI_INVALID_PARAMETER;
  }

  //
  // The Target input parameter is unnecessarily a pointer-to-pointer
  //
  TargetPtr = *Target;

  //
  // If target not initialized, return first target & LUN
  //
  if (!IsTargetInitialized (TargetPtr)) {
    ZeroMem (TargetPtr, TARGET_MAX_BYTES);
    *Lun = 0;
    return EFI_SUCCESS;
  }

  //
  // We only use first byte of target identifer
  //
  LastTarget = *TargetPtr;

  //
  // Increment (target, LUN) pair if valid on input
  //
  Dev = PVSCSI_FROM_PASS_THRU (This);
  if ((LastTarget > Dev->MaxTarget) || (*Lun > Dev->MaxLun)) {
    return EFI_INVALID_PARAMETER;
  }

  if (*Lun < Dev->MaxLun) {
    ++*Lun;
    return EFI_SUCCESS;
  }

  if (LastTarget < Dev->MaxTarget) {
    *Lun = 0;
    ++LastTarget;
    *TargetPtr = LastTarget;
    return EFI_SUCCESS;
  }

  return EFI_NOT_FOUND;
}

STATIC
EFI_STATUS
EFIAPI
PvScsiBuildDevicePath (
  IN EFI_EXT_SCSI_PASS_THRU_PROTOCOL  *This,
  IN UINT8                            *Target,
  IN UINT64                           Lun,
  IN OUT EFI_DEVICE_PATH_PROTOCOL     **DevicePath
  )
{
  UINT8             TargetValue;
  PVSCSI_DEV        *Dev;
  SCSI_DEVICE_PATH  *ScsiDevicePath;

  if (DevicePath == NULL) {
    return EFI_INVALID_PARAMETER;
  }

  //
  // We only use first byte of target identifer
  //
  TargetValue = *Target;

  Dev = PVSCSI_FROM_PASS_THRU (This);
  if ((TargetValue > Dev->MaxTarget) || (Lun > Dev->MaxLun)) {
    return EFI_NOT_FOUND;
  }

  ScsiDevicePath = AllocatePool (sizeof (*ScsiDevicePath));
  if (ScsiDevicePath == NULL) {
    return EFI_OUT_OF_RESOURCES;
  }

  ScsiDevicePath->Header.Type      = MESSAGING_DEVICE_PATH;
  ScsiDevicePath->Header.SubType   = MSG_SCSI_DP;
  ScsiDevicePath->Header.Length[0] = (UINT8)sizeof (*ScsiDevicePath);
  ScsiDevicePath->Header.Length[1] = (UINT8)(sizeof (*ScsiDevicePath) >> 8);
  ScsiDevicePath->Pun              = TargetValue;
  ScsiDevicePath->Lun              = (UINT16)Lun;

  *DevicePath = &ScsiDevicePath->Header;
  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
EFIAPI
PvScsiGetTargetLun (
  IN EFI_EXT_SCSI_PASS_THRU_PROTOCOL  *This,
  IN EFI_DEVICE_PATH_PROTOCOL         *DevicePath,
  OUT UINT8                           **Target,
  OUT UINT64                          *Lun
  )
{
  SCSI_DEVICE_PATH  *ScsiDevicePath;
  PVSCSI_DEV        *Dev;

  if ((DevicePath == NULL) || (Target == NULL) || (*Target == NULL) || (Lun == NULL)) {
    return EFI_INVALID_PARAMETER;
  }

  if ((DevicePath->Type    != MESSAGING_DEVICE_PATH) ||
      (DevicePath->SubType != MSG_SCSI_DP))
  {
    return EFI_UNSUPPORTED;
  }

  ScsiDevicePath = (SCSI_DEVICE_PATH *)DevicePath;
  Dev            = PVSCSI_FROM_PASS_THRU (This);
  if ((ScsiDevicePath->Pun > Dev->MaxTarget) ||
      (ScsiDevicePath->Lun > Dev->MaxLun))
  {
    return EFI_NOT_FOUND;
  }

  //
  // We only use first byte of target identifer
  //
  **Target = (UINT8)ScsiDevicePath->Pun;
  ZeroMem (*Target + 1, TARGET_MAX_BYTES - 1);
  *Lun = ScsiDevicePath->Lun;

  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
EFIAPI
PvScsiResetChannel (
  IN EFI_EXT_SCSI_PASS_THRU_PROTOCOL  *This
  )
{
  return EFI_UNSUPPORTED;
}

STATIC
EFI_STATUS
EFIAPI
PvScsiResetTargetLun (
  IN EFI_EXT_SCSI_PASS_THRU_PROTOCOL  *This,
  IN UINT8                            *Target,
  IN UINT64                           Lun
  )
{
  return EFI_UNSUPPORTED;
}

STATIC
EFI_STATUS
EFIAPI
PvScsiGetNextTarget (
  IN EFI_EXT_SCSI_PASS_THRU_PROTOCOL  *This,
  IN OUT UINT8                        **Target
  )
{
  UINT8       *TargetPtr;
  UINT8       LastTarget;
  PVSCSI_DEV  *Dev;

  if (Target == NULL) {
    return EFI_INVALID_PARAMETER;
  }

  //
  // The Target input parameter is unnecessarily a pointer-to-pointer
  //
  TargetPtr = *Target;

  //
  // If target not initialized, return first target
  //
  if (!IsTargetInitialized (TargetPtr)) {
    ZeroMem (TargetPtr, TARGET_MAX_BYTES);
    return EFI_SUCCESS;
  }

  //
  // We only use first byte of target identifer
  //
  LastTarget = *TargetPtr;

  //
  // Increment target if valid on input
  //
  Dev = PVSCSI_FROM_PASS_THRU (This);
  if (LastTarget > Dev->MaxTarget) {
    return EFI_INVALID_PARAMETER;
  }

  if (LastTarget < Dev->MaxTarget) {
    ++LastTarget;
    *TargetPtr = LastTarget;
    return EFI_SUCCESS;
  }

  return EFI_NOT_FOUND;
}

STATIC
EFI_STATUS
PvScsiSetPciAttributes (
  IN OUT PVSCSI_DEV  *Dev
  )
{
  EFI_STATUS  Status;

  //
  // Backup original PCI Attributes
  //
  Status = Dev->PciIo->Attributes (
                         Dev->PciIo,
                         EfiPciIoAttributeOperationGet,
                         0,
                         &Dev->OriginalPciAttributes
                         );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  //
  // Enable MMIO-Space & Bus-Mastering
  //
  Status = Dev->PciIo->Attributes (
                         Dev->PciIo,
                         EfiPciIoAttributeOperationEnable,
                         (EFI_PCI_IO_ATTRIBUTE_MEMORY |
                          EFI_PCI_IO_ATTRIBUTE_BUS_MASTER),
                         NULL
                         );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  //
  // Signal device supports 64-bit DMA addresses
  //
  Status = Dev->PciIo->Attributes (
                         Dev->PciIo,
                         EfiPciIoAttributeOperationEnable,
                         EFI_PCI_IO_ATTRIBUTE_DUAL_ADDRESS_CYCLE,
                         NULL
                         );
  if (EFI_ERROR (Status)) {
    //
    // Warn user that device will only be using 32-bit DMA addresses.
    //
    // Note that this does not prevent the device/driver from working
    // and therefore we only warn and continue as usual.
    //
    DEBUG ((
      DEBUG_WARN,
      "%a: failed to enable 64-bit DMA addresses\n",
      __FUNCTION__
      ));
  }

  return EFI_SUCCESS;
}

STATIC
VOID
PvScsiRestorePciAttributes (
  IN PVSCSI_DEV  *Dev
  )
{
  Dev->PciIo->Attributes (
                Dev->PciIo,
                EfiPciIoAttributeOperationSet,
                Dev->OriginalPciAttributes,
                NULL
                );
}

STATIC
EFI_STATUS
PvScsiAllocateSharedPages (
  IN PVSCSI_DEV        *Dev,
  IN UINTN             Pages,
  OUT VOID             **HostAddress,
  OUT PVSCSI_DMA_DESC  *DmaDesc
  )
{
  EFI_STATUS  Status;
  UINTN       NumberOfBytes;

  Status = Dev->PciIo->AllocateBuffer (
                         Dev->PciIo,
                         AllocateAnyPages,
                         EfiBootServicesData,
                         Pages,
                         HostAddress,
                         EFI_PCI_ATTRIBUTE_MEMORY_CACHED
                         );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  NumberOfBytes = EFI_PAGES_TO_SIZE (Pages);
  Status        = Dev->PciIo->Map (
                                Dev->PciIo,
                                EfiPciIoOperationBusMasterCommonBuffer,
                                *HostAddress,
                                &NumberOfBytes,
                                &DmaDesc->DeviceAddress,
                                &DmaDesc->Mapping
                                );
  if (EFI_ERROR (Status)) {
    goto FreeBuffer;
  }

  if (NumberOfBytes != EFI_PAGES_TO_SIZE (Pages)) {
    Status = EFI_OUT_OF_RESOURCES;
    goto Unmap;
  }

  return EFI_SUCCESS;

Unmap:
  Dev->PciIo->Unmap (Dev->PciIo, DmaDesc->Mapping);

FreeBuffer:
  Dev->PciIo->FreeBuffer (Dev->PciIo, Pages, *HostAddress);

  return Status;
}

STATIC
VOID
PvScsiFreeSharedPages (
  IN PVSCSI_DEV       *Dev,
  IN UINTN            Pages,
  IN VOID             *HostAddress,
  IN PVSCSI_DMA_DESC  *DmaDesc
  )
{
  Dev->PciIo->Unmap (Dev->PciIo, DmaDesc->Mapping);
  Dev->PciIo->FreeBuffer (Dev->PciIo, Pages, HostAddress);
}

STATIC
EFI_STATUS
PvScsiInitRings (
  IN OUT PVSCSI_DEV  *Dev
  )
{
  EFI_STATUS  Status;

  Status = PvScsiAllocateSharedPages (
             Dev,
             1,
             (VOID **)&Dev->RingDesc.RingState,
             &Dev->RingDesc.RingStateDmaDesc
             );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  ZeroMem (Dev->RingDesc.RingState, EFI_PAGE_SIZE);

  Status = PvScsiAllocateSharedPages (
             Dev,
             1,
             (VOID **)&Dev->RingDesc.RingReqs,
             &Dev->RingDesc.RingReqsDmaDesc
             );
  if (EFI_ERROR (Status)) {
    goto FreeRingState;
  }

  ZeroMem (Dev->RingDesc.RingReqs, EFI_PAGE_SIZE);

  Status = PvScsiAllocateSharedPages (
             Dev,
             1,
             (VOID **)&Dev->RingDesc.RingCmps,
             &Dev->RingDesc.RingCmpsDmaDesc
             );
  if (EFI_ERROR (Status)) {
    goto FreeRingReqs;
  }

  ZeroMem (Dev->RingDesc.RingCmps, EFI_PAGE_SIZE);

  return EFI_SUCCESS;

FreeRingReqs:
  PvScsiFreeSharedPages (
    Dev,
    1,
    Dev->RingDesc.RingReqs,
    &Dev->RingDesc.RingReqsDmaDesc
    );

FreeRingState:
  PvScsiFreeSharedPages (
    Dev,
    1,
    Dev->RingDesc.RingState,
    &Dev->RingDesc.RingStateDmaDesc
    );

  return Status;
}

STATIC
VOID
PvScsiFreeRings (
  IN OUT PVSCSI_DEV  *Dev
  )
{
  PvScsiFreeSharedPages (
    Dev,
    1,
    Dev->RingDesc.RingCmps,
    &Dev->RingDesc.RingCmpsDmaDesc
    );

  PvScsiFreeSharedPages (
    Dev,
    1,
    Dev->RingDesc.RingReqs,
    &Dev->RingDesc.RingReqsDmaDesc
    );

  PvScsiFreeSharedPages (
    Dev,
    1,
    Dev->RingDesc.RingState,
    &Dev->RingDesc.RingStateDmaDesc
    );
}

STATIC
EFI_STATUS
PvScsiSetupRings (
  IN OUT PVSCSI_DEV  *Dev
  )
{
  union {
    PVSCSI_CMD_DESC_SETUP_RINGS    Cmd;
    UINT32                         Uint32;
  } AlignedCmd;
  PVSCSI_CMD_DESC_SETUP_RINGS  *Cmd;

  Cmd = &AlignedCmd.Cmd;

  ZeroMem (Cmd, sizeof (*Cmd));
  Cmd->ReqRingNumPages = 1;
  Cmd->CmpRingNumPages = 1;
  Cmd->RingsStatePPN   = RShiftU64 (
                           Dev->RingDesc.RingStateDmaDesc.DeviceAddress,
                           EFI_PAGE_SHIFT
                           );
  Cmd->ReqRingPPNs[0] = RShiftU64 (
                          Dev->RingDesc.RingReqsDmaDesc.DeviceAddress,
                          EFI_PAGE_SHIFT
                          );
  Cmd->CmpRingPPNs[0] = RShiftU64 (
                          Dev->RingDesc.RingCmpsDmaDesc.DeviceAddress,
                          EFI_PAGE_SHIFT
                          );

  STATIC_ASSERT (
    sizeof (*Cmd) % sizeof (UINT32) == 0,
    "Cmd must be multiple of 32-bit words"
    );
  return PvScsiWriteCmdDesc (
           Dev,
           PvScsiCmdSetupRings,
           (UINT32 *)Cmd,
           sizeof (*Cmd) / sizeof (UINT32)
           );
}

STATIC
EFI_STATUS
PvScsiInit (
  IN OUT PVSCSI_DEV  *Dev
  )
{
  EFI_STATUS  Status;

  //
  // Init configuration
  //
  Dev->MaxTarget              = PcdGet8 (PcdPvScsiMaxTargetLimit);
  Dev->MaxLun                 = PcdGet8 (PcdPvScsiMaxLunLimit);
  Dev->WaitForCmpStallInUsecs = PcdGet32 (PcdPvScsiWaitForCmpStallInUsecs);

  //
  // Set PCI Attributes
  //
  Status = PvScsiSetPciAttributes (Dev);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  //
  // Reset adapter
  //
  Status = PvScsiResetAdapter (Dev);
  if (EFI_ERROR (Status)) {
    goto RestorePciAttributes;
  }

  //
  // Init PVSCSI rings
  //
  Status = PvScsiInitRings (Dev);
  if (EFI_ERROR (Status)) {
    goto RestorePciAttributes;
  }

  //
  // Allocate DMA communication buffer
  //
  Status = PvScsiAllocateSharedPages (
             Dev,
             EFI_SIZE_TO_PAGES (sizeof (*Dev->DmaBuf)),
             (VOID **)&Dev->DmaBuf,
             &Dev->DmaBufDmaDesc
             );
  if (EFI_ERROR (Status)) {
    goto FreeRings;
  }

  //
  // Setup rings against device
  //
  Status = PvScsiSetupRings (Dev);
  if (EFI_ERROR (Status)) {
    goto FreeDmaCommBuffer;
  }

  //
  // Populate the exported interface's attributes
  //
  Dev->PassThru.Mode             = &Dev->PassThruMode;
  Dev->PassThru.PassThru         = &PvScsiPassThru;
  Dev->PassThru.GetNextTargetLun = &PvScsiGetNextTargetLun;
  Dev->PassThru.BuildDevicePath  = &PvScsiBuildDevicePath;
  Dev->PassThru.GetTargetLun     = &PvScsiGetTargetLun;
  Dev->PassThru.ResetChannel     = &PvScsiResetChannel;
  Dev->PassThru.ResetTargetLun   = &PvScsiResetTargetLun;
  Dev->PassThru.GetNextTarget    = &PvScsiGetNextTarget;

  //
  // AdapterId is a target for which no handle will be created during bus scan.
  // Prevent any conflict with real devices.
  //
  Dev->PassThruMode.AdapterId = MAX_UINT32;

  //
  // Set both physical and logical attributes for non-RAID SCSI channel
  //
  Dev->PassThruMode.Attributes = EFI_EXT_SCSI_PASS_THRU_ATTRIBUTES_PHYSICAL |
                                 EFI_EXT_SCSI_PASS_THRU_ATTRIBUTES_LOGICAL;

  //
  // No restriction on transfer buffer alignment
  //
  Dev->PassThruMode.IoAlign = 0;

  return EFI_SUCCESS;

FreeDmaCommBuffer:
  PvScsiFreeSharedPages (
    Dev,
    EFI_SIZE_TO_PAGES (sizeof (*Dev->DmaBuf)),
    Dev->DmaBuf,
    &Dev->DmaBufDmaDesc
    );

FreeRings:
  PvScsiFreeRings (Dev);

RestorePciAttributes:
  PvScsiRestorePciAttributes (Dev);

  return Status;
}

STATIC
VOID
PvScsiUninit (
  IN OUT PVSCSI_DEV  *Dev
  )
{
  //
  // Reset device to:
  // - Make device stop processing all requests.
  // - Stop device usage of the rings.
  //
  // This is required to safely free the DMA communication buffer
  // and the rings.
  //
  PvScsiResetAdapter (Dev);

  //
  // Free DMA communication buffer
  //
  PvScsiFreeSharedPages (
    Dev,
    EFI_SIZE_TO_PAGES (sizeof (*Dev->DmaBuf)),
    Dev->DmaBuf,
    &Dev->DmaBufDmaDesc
    );

  PvScsiFreeRings (Dev);

  PvScsiRestorePciAttributes (Dev);
}

/**
  Event notification called by ExitBootServices()
**/
STATIC
VOID
EFIAPI
PvScsiExitBoot (
  IN  EFI_EVENT  Event,
  IN  VOID       *Context
  )
{
  PVSCSI_DEV  *Dev;

  Dev = Context;
  DEBUG ((DEBUG_VERBOSE, "%a: Context=0x%p\n", __FUNCTION__, Context));

  //
  // Reset the device to stop device usage of the rings.
  //
  // We allocated said rings in EfiBootServicesData type memory, and code
  // executing after ExitBootServices() is permitted to overwrite it.
  //
  PvScsiResetAdapter (Dev);
}

//
// Driver Binding
//

STATIC
EFI_STATUS
EFIAPI
PvScsiDriverBindingSupported (
  IN EFI_DRIVER_BINDING_PROTOCOL  *This,
  IN EFI_HANDLE                   ControllerHandle,
  IN EFI_DEVICE_PATH_PROTOCOL     *RemainingDevicePath OPTIONAL
  )
{
  EFI_STATUS           Status;
  EFI_PCI_IO_PROTOCOL  *PciIo;
  PCI_TYPE00           Pci;

  Status = gBS->OpenProtocol (
                  ControllerHandle,
                  &gEfiPciIoProtocolGuid,
                  (VOID **)&PciIo,
                  This->DriverBindingHandle,
                  ControllerHandle,
                  EFI_OPEN_PROTOCOL_BY_DRIVER
                  );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Status = PciIo->Pci.Read (
                        PciIo,
                        EfiPciIoWidthUint32,
                        0,
                        sizeof (Pci) / sizeof (UINT32),
                        &Pci
                        );
  if (EFI_ERROR (Status)) {
    goto Done;
  }

  if ((Pci.Hdr.VendorId != PCI_VENDOR_ID_VMWARE) ||
      (Pci.Hdr.DeviceId != PCI_DEVICE_ID_VMWARE_PVSCSI))
  {
    Status = EFI_UNSUPPORTED;
    goto Done;
  }

  Status = EFI_SUCCESS;

Done:
  gBS->CloseProtocol (
         ControllerHandle,
         &gEfiPciIoProtocolGuid,
         This->DriverBindingHandle,
         ControllerHandle
         );

  return Status;
}

STATIC
EFI_STATUS
EFIAPI
PvScsiDriverBindingStart (
  IN EFI_DRIVER_BINDING_PROTOCOL  *This,
  IN EFI_HANDLE                   ControllerHandle,
  IN EFI_DEVICE_PATH_PROTOCOL     *RemainingDevicePath OPTIONAL
  )
{
  PVSCSI_DEV  *Dev;
  EFI_STATUS  Status;

  Dev = (PVSCSI_DEV *)AllocateZeroPool (sizeof (*Dev));
  if (Dev == NULL) {
    return EFI_OUT_OF_RESOURCES;
  }

  Status = gBS->OpenProtocol (
                  ControllerHandle,
                  &gEfiPciIoProtocolGuid,
                  (VOID **)&Dev->PciIo,
                  This->DriverBindingHandle,
                  ControllerHandle,
                  EFI_OPEN_PROTOCOL_BY_DRIVER
                  );
  if (EFI_ERROR (Status)) {
    goto FreePvScsi;
  }

  Status = PvScsiInit (Dev);
  if (EFI_ERROR (Status)) {
    goto ClosePciIo;
  }

  Status = gBS->CreateEvent (
                  EVT_SIGNAL_EXIT_BOOT_SERVICES,
                  TPL_CALLBACK,
                  &PvScsiExitBoot,
                  Dev,
                  &Dev->ExitBoot
                  );
  if (EFI_ERROR (Status)) {
    goto UninitDev;
  }

  //
  // Setup complete, attempt to export the driver instance's PassThru interface
  //
  Dev->Signature = PVSCSI_SIG;
  Status         = gBS->InstallProtocolInterface (
                          &ControllerHandle,
                          &gEfiExtScsiPassThruProtocolGuid,
                          EFI_NATIVE_INTERFACE,
                          &Dev->PassThru
                          );
  if (EFI_ERROR (Status)) {
    goto CloseExitBoot;
  }

  return EFI_SUCCESS;

CloseExitBoot:
  gBS->CloseEvent (Dev->ExitBoot);

UninitDev:
  PvScsiUninit (Dev);

ClosePciIo:
  gBS->CloseProtocol (
         ControllerHandle,
         &gEfiPciIoProtocolGuid,
         This->DriverBindingHandle,
         ControllerHandle
         );

FreePvScsi:
  FreePool (Dev);

  return Status;
}

STATIC
EFI_STATUS
EFIAPI
PvScsiDriverBindingStop (
  IN EFI_DRIVER_BINDING_PROTOCOL  *This,
  IN EFI_HANDLE                   ControllerHandle,
  IN UINTN                        NumberOfChildren,
  IN EFI_HANDLE                   *ChildHandleBuffer
  )
{
  EFI_STATUS                       Status;
  EFI_EXT_SCSI_PASS_THRU_PROTOCOL  *PassThru;
  PVSCSI_DEV                       *Dev;

  Status = gBS->OpenProtocol (
                  ControllerHandle,
                  &gEfiExtScsiPassThruProtocolGuid,
                  (VOID **)&PassThru,
                  This->DriverBindingHandle,
                  ControllerHandle,
                  EFI_OPEN_PROTOCOL_GET_PROTOCOL // Lookup only
                  );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Dev = PVSCSI_FROM_PASS_THRU (PassThru);

  Status = gBS->UninstallProtocolInterface (
                  ControllerHandle,
                  &gEfiExtScsiPassThruProtocolGuid,
                  &Dev->PassThru
                  );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  gBS->CloseEvent (Dev->ExitBoot);

  PvScsiUninit (Dev);

  gBS->CloseProtocol (
         ControllerHandle,
         &gEfiPciIoProtocolGuid,
         This->DriverBindingHandle,
         ControllerHandle
         );

  FreePool (Dev);

  return EFI_SUCCESS;
}

STATIC EFI_DRIVER_BINDING_PROTOCOL  mPvScsiDriverBinding = {
  &PvScsiDriverBindingSupported,
  &PvScsiDriverBindingStart,
  &PvScsiDriverBindingStop,
  PVSCSI_BINDING_VERSION,
  NULL, // ImageHandle, filled by EfiLibInstallDriverBindingComponentName2()
  NULL  // DriverBindingHandle, filled as well
};

//
// Component Name
//

STATIC EFI_UNICODE_STRING_TABLE  mDriverNameTable[] = {
  { "eng;en", L"PVSCSI Host Driver" },
  { NULL,     NULL                  }
};

STATIC EFI_COMPONENT_NAME_PROTOCOL  mComponentName;

STATIC
EFI_STATUS
EFIAPI
PvScsiGetDriverName (
  IN  EFI_COMPONENT_NAME_PROTOCOL  *This,
  IN  CHAR8                        *Language,
  OUT CHAR16                       **DriverName
  )
{
  return LookupUnicodeString2 (
           Language,
           This->SupportedLanguages,
           mDriverNameTable,
           DriverName,
           (BOOLEAN)(This == &mComponentName) // Iso639Language
           );
}

STATIC
EFI_STATUS
EFIAPI
PvScsiGetDeviceName (
  IN  EFI_COMPONENT_NAME_PROTOCOL  *This,
  IN  EFI_HANDLE                   DeviceHandle,
  IN  EFI_HANDLE                   ChildHandle,
  IN  CHAR8                        *Language,
  OUT CHAR16                       **ControllerName
  )
{
  return EFI_UNSUPPORTED;
}

STATIC EFI_COMPONENT_NAME_PROTOCOL  mComponentName = {
  &PvScsiGetDriverName,
  &PvScsiGetDeviceName,
  "eng" // SupportedLanguages, ISO 639-2 language codes
};

STATIC EFI_COMPONENT_NAME2_PROTOCOL  mComponentName2 = {
  (EFI_COMPONENT_NAME2_GET_DRIVER_NAME)&PvScsiGetDriverName,
  (EFI_COMPONENT_NAME2_GET_CONTROLLER_NAME)&PvScsiGetDeviceName,
  "en" // SupportedLanguages, RFC 4646 language codes
};

//
// Entry Point
//

EFI_STATUS
EFIAPI
PvScsiEntryPoint (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  return EfiLibInstallDriverBindingComponentName2 (
           ImageHandle,
           SystemTable,
           &mPvScsiDriverBinding,
           ImageHandle,
           &mComponentName,
           &mComponentName2
           );
}
