/** @file

  This driver produces Extended SCSI Pass Thru Protocol instances for
  LSI 53C895A SCSI devices.

  Copyright (C) 2020, SUSE LLC.

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

**/

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

#include "LsiScsi.h"

STATIC
EFI_STATUS
Out8 (
  IN LSI_SCSI_DEV  *Dev,
  IN UINT32        Addr,
  IN UINT8         Data
  )
{
  return Dev->PciIo->Io.Write (
                          Dev->PciIo,
                          EfiPciIoWidthUint8,
                          PCI_BAR_IDX0,
                          Addr,
                          1,
                          &Data
                          );
}

STATIC
EFI_STATUS
Out32 (
  IN LSI_SCSI_DEV  *Dev,
  IN UINT32        Addr,
  IN UINT32        Data
  )
{
  return Dev->PciIo->Io.Write (
                          Dev->PciIo,
                          EfiPciIoWidthUint32,
                          PCI_BAR_IDX0,
                          Addr,
                          1,
                          &Data
                          );
}

STATIC
EFI_STATUS
In8 (
  IN  LSI_SCSI_DEV  *Dev,
  IN  UINT32        Addr,
  OUT UINT8         *Data
  )
{
  return Dev->PciIo->Io.Read (
                          Dev->PciIo,
                          EfiPciIoWidthUint8,
                          PCI_BAR_IDX0,
                          Addr,
                          1,
                          Data
                          );
}

STATIC
EFI_STATUS
In32 (
  IN  LSI_SCSI_DEV  *Dev,
  IN  UINT32        Addr,
  OUT UINT32        *Data
  )
{
  return Dev->PciIo->Io.Read (
                          Dev->PciIo,
                          EfiPciIoWidthUint32,
                          PCI_BAR_IDX0,
                          Addr,
                          1,
                          Data
                          );
}

STATIC
EFI_STATUS
LsiScsiReset (
  IN LSI_SCSI_DEV  *Dev
  )
{
  return Out8 (Dev, LSI_REG_ISTAT0, LSI_ISTAT0_SRST);
}

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;
}

/**

  Check the request packet from the Extended SCSI Pass Thru Protocol. The
  request packet is modified, to be forwarded outwards by LsiScsiPassThru(),
  if invalid or unsupported parameters are detected.

  @param[in] Dev          The LSI 53C895A SCSI device the packet targets.

  @param[in] Target       The SCSI target controlled by the LSI 53C895A SCSI
                          device.

  @param[in] Lun          The Logical Unit Number under the SCSI target.

  @param[in out] Packet   The Extended SCSI Pass Thru Protocol packet.


  @retval EFI_SUCCESS  The Extended SCSI Pass Thru Protocol packet was valid.

  @return              Otherwise, invalid or unsupported parameters were
                       detected. Status codes are meant for direct forwarding
                       by the EFI_EXT_SCSI_PASS_THRU_PROTOCOL.PassThru()
                       implementation.

 **/
STATIC
EFI_STATUS
LsiScsiCheckRequest (
  IN LSI_SCSI_DEV                                    *Dev,
  IN UINT8                                           Target,
  IN UINT64                                          Lun,
  IN OUT EFI_EXT_SCSI_PASS_THRU_SCSI_REQUEST_PACKET  *Packet
  )
{
  if ((Target > Dev->MaxTarget) || (Lun > Dev->MaxLun) ||
      (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)
       )
      )
      )
  {
    return EFI_INVALID_PARAMETER;
  }

  if ((Packet->DataDirection == EFI_EXT_SCSI_DATA_DIRECTION_BIDIRECTIONAL) ||
      ((Packet->InTransferLength > 0) && (Packet->OutTransferLength > 0)) ||
      (Packet->CdbLength > sizeof Dev->Dma->Cdb))
  {
    return EFI_UNSUPPORTED;
  }

  if (Packet->InTransferLength > sizeof Dev->Dma->Data) {
    Packet->InTransferLength = sizeof Dev->Dma->Data;
    return ReportHostAdapterOverrunError (Packet);
  }

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

  return EFI_SUCCESS;
}

/**

  Interpret the request packet from the Extended SCSI Pass Thru Protocol and
  compose the script to submit the command and data to the controller.

  @param[in] Dev          The LSI 53C895A SCSI device the packet targets.

  @param[in] Target       The SCSI target controlled by the LSI 53C895A SCSI
                          device.

  @param[in] Lun          The Logical Unit Number under the SCSI target.

  @param[in out] Packet   The Extended SCSI Pass Thru Protocol packet.


  @retval EFI_SUCCESS  The Extended SCSI Pass Thru Protocol packet was valid.

  @return              Otherwise, invalid or unsupported parameters were
                       detected. Status codes are meant for direct forwarding
                       by the EFI_EXT_SCSI_PASS_THRU_PROTOCOL.PassThru()
                       implementation.

 **/
STATIC
EFI_STATUS
LsiScsiProcessRequest (
  IN LSI_SCSI_DEV                                    *Dev,
  IN UINT8                                           Target,
  IN UINT64                                          Lun,
  IN OUT EFI_EXT_SCSI_PASS_THRU_SCSI_REQUEST_PACKET  *Packet
  )
{
  EFI_STATUS  Status;
  UINT32      *Script;
  UINT8       *Cdb;
  UINT8       *MsgOut;
  UINT8       *MsgIn;
  UINT8       *ScsiStatus;
  UINT8       *Data;
  UINT8       DStat;
  UINT8       SIst0;
  UINT8       SIst1;
  UINT32      Csbc;
  UINT32      CsbcBase;
  UINT32      Transferred;

  Script     = Dev->Dma->Script;
  Cdb        = Dev->Dma->Cdb;
  Data       = Dev->Dma->Data;
  MsgIn      = Dev->Dma->MsgIn;
  MsgOut     = &Dev->Dma->MsgOut;
  ScsiStatus = &Dev->Dma->Status;

  *ScsiStatus = 0xFF;

  DStat = 0;
  SIst0 = 0;
  SIst1 = 0;

  SetMem (Cdb, sizeof Dev->Dma->Cdb, 0x00);
  CopyMem (Cdb, Packet->Cdb, Packet->CdbLength);

  //
  // Fetch the first Cumulative SCSI Byte Count (CSBC).
  //
  // CSBC is a cumulative counter of the actual number of bytes that have been
  // transferred across the SCSI bus during data phases, i.e. it will not
  // count bytes sent in command, status, message in and out phases.
  //
  Status = In32 (Dev, LSI_REG_CSBC, &CsbcBase);
  if (EFI_ERROR (Status)) {
    goto Error;
  }

  //
  // Clean up the DMA buffer for the script.
  //
  SetMem (Script, sizeof Dev->Dma->Script, 0x00);

  //
  // Compose the script to transfer data between the host and the device.
  //
  // References:
  //   * LSI53C895A PCI to Ultra2 SCSI Controller Version 2.2
  //     - Chapter 5 SCSI SCRIPT Instruction Set
  //   * SEABIOS lsi-scsi driver
  //
  // All instructions used here consist of 2 32bit words. The first word
  // contains the command to execute. The second word is loaded into the
  // DMA SCRIPTS Pointer Save (DSPS) register as either the DMA address
  // for data transmission or the address/offset for the jump command.
  // Some commands, such as the selection of the target, don't need to
  // transfer data through DMA or jump to another instruction, then DSPS
  // has to be zero.
  //
  // There are 3 major parts in this script. The first part (1~3) contains
  // the instructions to select target and LUN and send the SCSI command
  // from the request packet. The second part (4~7) is to handle the
  // potential disconnection and prepare for the data transmission. The
  // instructions in the third part (8~10) transmit the given data and
  // collect the result. Instruction 11 raises the interrupt and marks the
  // end of the script.
  //

  //
  // 1. Select target.
  //
  *Script++ = LSI_INS_TYPE_IO | LSI_INS_IO_OPC_SEL | (UINT32)Target << 16;
  *Script++ = 0x00000000;

  //
  // 2. Select LUN.
  //
  *MsgOut   = 0x80 | (UINT8)Lun;  // 0x80: Identify bit
  *Script++ = LSI_INS_TYPE_BLK | LSI_INS_BLK_SCSIP_MSG_OUT |
              (UINT32)sizeof Dev->Dma->MsgOut;
  *Script++ = LSI_SCSI_DMA_ADDR (Dev, MsgOut);

  //
  // 3. Send the SCSI Command.
  //
  *Script++ = LSI_INS_TYPE_BLK | LSI_INS_BLK_SCSIP_CMD |
              (UINT32)sizeof Dev->Dma->Cdb;
  *Script++ = LSI_SCSI_DMA_ADDR (Dev, Cdb);

  //
  // 4. Check whether the current SCSI phase is "Message In" or not
  //    and jump to 7 if it is.
  //    Note: LSI_INS_TC_RA stands for "Relative Address Mode", so the
  //          offset 0x18 in the second word means jumping forward
  //          3 (0x18/8) instructions.
  //
  *Script++ = LSI_INS_TYPE_TC | LSI_INS_TC_OPC_JMP |
              LSI_INS_TC_SCSIP_MSG_IN | LSI_INS_TC_RA |
              LSI_INS_TC_CP;
  *Script++ = 0x00000018;

  //
  // 5. Read "Message" from the initiator to trigger reselect.
  //
  *Script++ = LSI_INS_TYPE_BLK | LSI_INS_BLK_SCSIP_MSG_IN |
              (UINT32)sizeof Dev->Dma->MsgIn;
  *Script++ = LSI_SCSI_DMA_ADDR (Dev, MsgIn);

  //
  // 6. Wait reselect.
  //
  *Script++ = LSI_INS_TYPE_IO | LSI_INS_IO_OPC_WAIT_RESEL;
  *Script++ = 0x00000000;

  //
  // 7. Read "Message" from the initiator again
  //
  *Script++ = LSI_INS_TYPE_BLK | LSI_INS_BLK_SCSIP_MSG_IN |
              (UINT32)sizeof Dev->Dma->MsgIn;
  *Script++ = LSI_SCSI_DMA_ADDR (Dev, MsgIn);

  //
  // 8. Set the DMA command for the read/write operations.
  //    Note: Some requests, e.g. "TEST UNIT READY", do not come with
  //          allocated InDataBuffer or OutDataBuffer. We skip the DMA
  //          data command for those requests or this script would fail
  //          with LSI_SIST0_SGE due to the zero data length.
  //
  // LsiScsiCheckRequest() prevents both integer overflows in the command
  // opcodes, and buffer overflows.
  //
  if (Packet->InTransferLength > 0) {
    ASSERT (Packet->DataDirection == EFI_EXT_SCSI_DATA_DIRECTION_READ);
    ASSERT (Packet->InTransferLength <= sizeof Dev->Dma->Data);
    *Script++ = LSI_INS_TYPE_BLK | LSI_INS_BLK_SCSIP_DAT_IN |
                Packet->InTransferLength;
    *Script++ = LSI_SCSI_DMA_ADDR (Dev, Data);
  } else if (Packet->OutTransferLength > 0) {
    ASSERT (Packet->DataDirection == EFI_EXT_SCSI_DATA_DIRECTION_WRITE);
    ASSERT (Packet->OutTransferLength <= sizeof Dev->Dma->Data);
    CopyMem (Data, Packet->OutDataBuffer, Packet->OutTransferLength);
    *Script++ = LSI_INS_TYPE_BLK | LSI_INS_BLK_SCSIP_DAT_OUT |
                Packet->OutTransferLength;
    *Script++ = LSI_SCSI_DMA_ADDR (Dev, Data);
  }

  //
  // 9. Get the SCSI status.
  //
  *Script++ = LSI_INS_TYPE_BLK | LSI_INS_BLK_SCSIP_STAT |
              (UINT32)sizeof Dev->Dma->Status;
  *Script++ = LSI_SCSI_DMA_ADDR (Dev, Status);

  //
  // 10. Get the SCSI message.
  //
  *Script++ = LSI_INS_TYPE_BLK | LSI_INS_BLK_SCSIP_MSG_IN |
              (UINT32)sizeof Dev->Dma->MsgIn;
  *Script++ = LSI_SCSI_DMA_ADDR (Dev, MsgIn);

  //
  // 11. Raise the interrupt to end the script.
  //
  *Script++ = LSI_INS_TYPE_TC | LSI_INS_TC_OPC_INT |
              LSI_INS_TC_SCSIP_DAT_OUT | LSI_INS_TC_JMP;
  *Script++ = 0x00000000;

  //
  // Make sure the size of the script doesn't exceed the buffer.
  //
  ASSERT (Script <= Dev->Dma->Script + ARRAY_SIZE (Dev->Dma->Script));

  //
  // The controller starts to execute the script once the DMA Script
  // Pointer (DSP) register is set.
  //
  Status = Out32 (Dev, LSI_REG_DSP, LSI_SCSI_DMA_ADDR (Dev, Script));
  if (EFI_ERROR (Status)) {
    goto Error;
  }

  //
  // Poll the device registers (DSTAT, SIST0, and SIST1) until the SIR
  // bit sets.
  //
  for ( ; ;) {
    Status = In8 (Dev, LSI_REG_DSTAT, &DStat);
    if (EFI_ERROR (Status)) {
      goto Error;
    }

    Status = In8 (Dev, LSI_REG_SIST0, &SIst0);
    if (EFI_ERROR (Status)) {
      goto Error;
    }

    Status = In8 (Dev, LSI_REG_SIST1, &SIst1);
    if (EFI_ERROR (Status)) {
      goto Error;
    }

    if ((SIst0 != 0) || (SIst1 != 0)) {
      goto Error;
    }

    //
    // Check the SIR (SCRIPTS Interrupt Instruction Received) bit.
    //
    if (DStat & LSI_DSTAT_SIR) {
      break;
    }

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

  //
  // Check if everything is good.
  //   SCSI Message Code 0x00: COMMAND COMPLETE
  //   SCSI Status  Code 0x00: Good
  //
  if ((MsgIn[0] != 0) || (*ScsiStatus != 0)) {
    goto Error;
  }

  //
  // Fetch CSBC again to calculate the transferred bytes and update
  // InTransferLength/OutTransferLength.
  //
  // Note: The number of transferred bytes is bounded by
  //       "sizeof Dev->Dma->Data", so it's safe to subtract CsbcBase
  //       from Csbc. If the CSBC register wraps around, the correct
  //       difference is ensured by the standard C modular arithmetic.
  //
  Status = In32 (Dev, LSI_REG_CSBC, &Csbc);
  if (EFI_ERROR (Status)) {
    goto Error;
  }

  Transferred = Csbc - CsbcBase;
  if (Packet->InTransferLength > 0) {
    if (Transferred <= Packet->InTransferLength) {
      Packet->InTransferLength = Transferred;
    } else {
      goto Error;
    }
  } else if (Packet->OutTransferLength > 0) {
    if (Transferred <= Packet->OutTransferLength) {
      Packet->OutTransferLength = Transferred;
    } else {
      goto Error;
    }
  }

  //
  // Copy Data to InDataBuffer if necessary.
  //
  if (Packet->DataDirection == EFI_EXT_SCSI_DATA_DIRECTION_READ) {
    CopyMem (Packet->InDataBuffer, Data, Packet->InTransferLength);
  }

  //
  // Always set SenseDataLength to 0.
  // The instructions of LSI53C895A don't reply sense data. Instead, it
  // relies on the SCSI command, "REQUEST SENSE", to get sense data. We set
  // SenseDataLength to 0 to notify ScsiDiskDxe that there is no sense data
  // written even if this request is processed successfully, so that It will
  // issue "REQUEST SENSE" later to retrieve sense data.
  //
  Packet->SenseDataLength   = 0;
  Packet->HostAdapterStatus = EFI_EXT_SCSI_STATUS_HOST_ADAPTER_OK;
  Packet->TargetStatus      = EFI_EXT_SCSI_STATUS_TARGET_GOOD;

  return EFI_SUCCESS;

Error:
  DEBUG ((
    DEBUG_VERBOSE,
    "%a: dstat: %02X, sist0: %02X, sist1: %02X\n",
    __func__,
    DStat,
    SIst0,
    SIst1
    ));
  //
  // Update the request packet to reflect the status.
  //
  if (*ScsiStatus != 0xFF) {
    Packet->TargetStatus = *ScsiStatus;
  } else {
    Packet->TargetStatus = EFI_EXT_SCSI_STATUS_TARGET_TASK_ABORTED;
  }

  if (SIst0 & LSI_SIST0_PAR) {
    Packet->HostAdapterStatus = EFI_EXT_SCSI_STATUS_HOST_ADAPTER_PARITY_ERROR;
  } else if (SIst0 & LSI_SIST0_RST) {
    Packet->HostAdapterStatus = EFI_EXT_SCSI_STATUS_HOST_ADAPTER_BUS_RESET;
  } else if (SIst0 & LSI_SIST0_UDC) {
    //
    // The target device is disconnected unexpectedly. According to UEFI spec,
    // this is TIMEOUT_COMMAND.
    //
    Packet->HostAdapterStatus = EFI_EXT_SCSI_STATUS_HOST_ADAPTER_TIMEOUT_COMMAND;
  } else if (SIst0 & LSI_SIST0_SGE) {
    Packet->HostAdapterStatus = EFI_EXT_SCSI_STATUS_HOST_ADAPTER_DATA_OVERRUN_UNDERRUN;
  } else if (SIst1 & LSI_SIST1_HTH) {
    Packet->HostAdapterStatus = EFI_EXT_SCSI_STATUS_HOST_ADAPTER_TIMEOUT;
  } else if (SIst1 & LSI_SIST1_GEN) {
    Packet->HostAdapterStatus = EFI_EXT_SCSI_STATUS_HOST_ADAPTER_TIMEOUT;
  } else if (SIst1 & LSI_SIST1_STO) {
    Packet->HostAdapterStatus = EFI_EXT_SCSI_STATUS_HOST_ADAPTER_SELECTION_TIMEOUT;
  } else {
    Packet->HostAdapterStatus = EFI_EXT_SCSI_STATUS_HOST_ADAPTER_OTHER;
  }

  //
  // SenseData may be used to inspect the error. Since we don't set sense data,
  // SenseDataLength has to be 0.
  //
  Packet->SenseDataLength = 0;

  return EFI_DEVICE_ERROR;
}

//
// The next seven functions implement EFI_EXT_SCSI_PASS_THRU_PROTOCOL
// for the LSI 53C895A SCSI Controller. Refer to UEFI Spec 2.3.1 + Errata C,
// sections
// - 14.1 SCSI Driver Model Overview,
// - 14.7 Extended SCSI Pass Thru Protocol.
//

EFI_STATUS
EFIAPI
LsiScsiPassThru (
  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
  )
{
  EFI_STATUS    Status;
  LSI_SCSI_DEV  *Dev;

  Dev    = LSI_SCSI_FROM_PASS_THRU (This);
  Status = LsiScsiCheckRequest (Dev, *Target, Lun, Packet);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  return LsiScsiProcessRequest (Dev, *Target, Lun, Packet);
}

EFI_STATUS
EFIAPI
LsiScsiGetNextTargetLun (
  IN EFI_EXT_SCSI_PASS_THRU_PROTOCOL  *This,
  IN OUT UINT8                        **TargetPointer,
  IN OUT UINT64                       *Lun
  )
{
  LSI_SCSI_DEV  *Dev;
  UINTN         Idx;
  UINT8         *Target;
  UINT16        LastTarget;

  //
  // the TargetPointer input parameter is unnecessarily a pointer-to-pointer
  //
  Target = *TargetPointer;

  //
  // Search for first non-0xFF byte. If not found, return first target & LUN.
  //
  for (Idx = 0; Idx < TARGET_MAX_BYTES && Target[Idx] == 0xFF; ++Idx) {
  }

  if (Idx == TARGET_MAX_BYTES) {
    SetMem (Target, TARGET_MAX_BYTES, 0x00);
    *Lun = 0;
    return EFI_SUCCESS;
  }

  CopyMem (&LastTarget, Target, sizeof LastTarget);

  //
  // increment (target, LUN) pair if valid on input
  //
  Dev = LSI_SCSI_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;
    CopyMem (Target, &LastTarget, sizeof LastTarget);
    return EFI_SUCCESS;
  }

  return EFI_NOT_FOUND;
}

EFI_STATUS
EFIAPI
LsiScsiBuildDevicePath (
  IN EFI_EXT_SCSI_PASS_THRU_PROTOCOL  *This,
  IN UINT8                            *Target,
  IN UINT64                           Lun,
  IN OUT EFI_DEVICE_PATH_PROTOCOL     **DevicePath
  )
{
  UINT16            TargetValue;
  LSI_SCSI_DEV      *Dev;
  SCSI_DEVICE_PATH  *ScsiDevicePath;

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

  CopyMem (&TargetValue, Target, sizeof TargetValue);
  Dev = LSI_SCSI_FROM_PASS_THRU (This);
  if ((TargetValue > Dev->MaxTarget) || (Lun > Dev->MaxLun) || (Lun > 0xFFFF)) {
    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;
}

EFI_STATUS
EFIAPI
LsiScsiGetTargetLun (
  IN  EFI_EXT_SCSI_PASS_THRU_PROTOCOL  *This,
  IN  EFI_DEVICE_PATH_PROTOCOL         *DevicePath,
  OUT UINT8                            **TargetPointer,
  OUT UINT64                           *Lun
  )
{
  SCSI_DEVICE_PATH  *ScsiDevicePath;
  LSI_SCSI_DEV      *Dev;
  UINT8             *Target;

  if ((DevicePath == NULL) || (TargetPointer == NULL) || (*TargetPointer == 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            = LSI_SCSI_FROM_PASS_THRU (This);
  if ((ScsiDevicePath->Pun > Dev->MaxTarget) ||
      (ScsiDevicePath->Lun > Dev->MaxLun))
  {
    return EFI_NOT_FOUND;
  }

  Target = *TargetPointer;
  ZeroMem (Target, TARGET_MAX_BYTES);
  CopyMem (Target, &ScsiDevicePath->Pun, sizeof ScsiDevicePath->Pun);
  *Lun = ScsiDevicePath->Lun;

  return EFI_SUCCESS;
}

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

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

EFI_STATUS
EFIAPI
LsiScsiGetNextTarget (
  IN EFI_EXT_SCSI_PASS_THRU_PROTOCOL  *This,
  IN OUT UINT8                        **TargetPointer
  )
{
  LSI_SCSI_DEV  *Dev;
  UINTN         Idx;
  UINT8         *Target;
  UINT16        LastTarget;

  //
  // the TargetPointer input parameter is unnecessarily a pointer-to-pointer
  //
  Target = *TargetPointer;

  //
  // Search for first non-0xFF byte. If not found, return first target.
  //
  for (Idx = 0; Idx < TARGET_MAX_BYTES && Target[Idx] == 0xFF; ++Idx) {
  }

  if (Idx == TARGET_MAX_BYTES) {
    SetMem (Target, TARGET_MAX_BYTES, 0x00);
    return EFI_SUCCESS;
  }

  CopyMem (&LastTarget, Target, sizeof LastTarget);

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

  if (LastTarget < Dev->MaxTarget) {
    ++LastTarget;
    CopyMem (Target, &LastTarget, sizeof LastTarget);
    return EFI_SUCCESS;
  }

  return EFI_NOT_FOUND;
}

STATIC
VOID
EFIAPI
LsiScsiExitBoot (
  IN  EFI_EVENT  Event,
  IN  VOID       *Context
  )
{
  LSI_SCSI_DEV  *Dev;

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

//
// Probe, start and stop functions of this driver, called by the DXE core for
// specific devices.
//
// The following specifications document these interfaces:
// - Driver Writer's Guide for UEFI 2.3.1 v1.01, 9 Driver Binding Protocol
// - UEFI Spec 2.3.1 + Errata C, 10.1 EFI Driver Binding Protocol
//

EFI_STATUS
EFIAPI
LsiScsiControllerSupported (
  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 == LSI_LOGIC_PCI_VENDOR_ID) &&
      (Pci.Hdr.DeviceId == LSI_53C895A_PCI_DEVICE_ID))
  {
    Status = EFI_SUCCESS;
  } else {
    Status = EFI_UNSUPPORTED;
  }

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

EFI_STATUS
EFIAPI
LsiScsiControllerStart (
  IN EFI_DRIVER_BINDING_PROTOCOL  *This,
  IN EFI_HANDLE                   ControllerHandle,
  IN EFI_DEVICE_PATH_PROTOCOL     *RemainingDevicePath OPTIONAL
  )
{
  EFI_STATUS    Status;
  LSI_SCSI_DEV  *Dev;
  UINTN         Pages;
  UINTN         BytesMapped;

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

  Dev->Signature = LSI_SCSI_DEV_SIGNATURE;

  STATIC_ASSERT (
    FixedPcdGet8 (PcdLsiScsiMaxTargetLimit) < 8,
    "LSI 53C895A supports targets [0..7]"
    );
  STATIC_ASSERT (
    FixedPcdGet8 (PcdLsiScsiMaxLunLimit) < 128,
    "LSI 53C895A supports LUNs [0..127]"
    );
  Dev->MaxTarget        = PcdGet8 (PcdLsiScsiMaxTargetLimit);
  Dev->MaxLun           = PcdGet8 (PcdLsiScsiMaxLunLimit);
  Dev->StallPerPollUsec = PcdGet32 (PcdLsiScsiStallPerPollUsec);

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

  Status = Dev->PciIo->Attributes (
                         Dev->PciIo,
                         EfiPciIoAttributeOperationGet,
                         0,
                         &Dev->OrigPciAttrs
                         );
  if (EFI_ERROR (Status)) {
    goto CloseProtocol;
  }

  //
  // Enable I/O Space & Bus-Mastering
  //
  Status = Dev->PciIo->Attributes (
                         Dev->PciIo,
                         EfiPciIoAttributeOperationEnable,
                         (EFI_PCI_IO_ATTRIBUTE_IO |
                          EFI_PCI_IO_ATTRIBUTE_BUS_MASTER),
                         NULL
                         );
  if (EFI_ERROR (Status)) {
    goto CloseProtocol;
  }

  //
  // Create buffers for data transfer
  //
  Pages  = EFI_SIZE_TO_PAGES (sizeof (*Dev->Dma));
  Status = Dev->PciIo->AllocateBuffer (
                         Dev->PciIo,
                         AllocateAnyPages,
                         EfiBootServicesData,
                         Pages,
                         (VOID **)&Dev->Dma,
                         EFI_PCI_ATTRIBUTE_MEMORY_CACHED
                         );
  if (EFI_ERROR (Status)) {
    goto RestoreAttributes;
  }

  BytesMapped = EFI_PAGES_TO_SIZE (Pages);
  Status      = Dev->PciIo->Map (
                              Dev->PciIo,
                              EfiPciIoOperationBusMasterCommonBuffer,
                              Dev->Dma,
                              &BytesMapped,
                              &Dev->DmaPhysical,
                              &Dev->DmaMapping
                              );
  if (EFI_ERROR (Status)) {
    goto FreeBuffer;
  }

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

  Status = LsiScsiReset (Dev);
  if (EFI_ERROR (Status)) {
    goto Unmap;
  }

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

  //
  // Host adapter channel, doesn't exist
  //
  Dev->PassThruMode.AdapterId  = MAX_UINT32;
  Dev->PassThruMode.Attributes =
    EFI_EXT_SCSI_PASS_THRU_ATTRIBUTES_PHYSICAL |
    EFI_EXT_SCSI_PASS_THRU_ATTRIBUTES_LOGICAL;

  Dev->PassThru.Mode             = &Dev->PassThruMode;
  Dev->PassThru.PassThru         = &LsiScsiPassThru;
  Dev->PassThru.GetNextTargetLun = &LsiScsiGetNextTargetLun;
  Dev->PassThru.BuildDevicePath  = &LsiScsiBuildDevicePath;
  Dev->PassThru.GetTargetLun     = &LsiScsiGetTargetLun;
  Dev->PassThru.ResetChannel     = &LsiScsiResetChannel;
  Dev->PassThru.ResetTargetLun   = &LsiScsiResetTargetLun;
  Dev->PassThru.GetNextTarget    = &LsiScsiGetNextTarget;

  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:
  LsiScsiReset (Dev);

Unmap:
  Dev->PciIo->Unmap (
                Dev->PciIo,
                Dev->DmaMapping
                );

FreeBuffer:
  Dev->PciIo->FreeBuffer (
                Dev->PciIo,
                Pages,
                Dev->Dma
                );

RestoreAttributes:
  Dev->PciIo->Attributes (
                Dev->PciIo,
                EfiPciIoAttributeOperationSet,
                Dev->OrigPciAttrs,
                NULL
                );

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

FreePool:
  FreePool (Dev);

  return Status;
}

EFI_STATUS
EFIAPI
LsiScsiControllerStop (
  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;
  LSI_SCSI_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 = LSI_SCSI_FROM_PASS_THRU (PassThru);

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

  gBS->CloseEvent (Dev->ExitBoot);

  LsiScsiReset (Dev);

  Dev->PciIo->Unmap (
                Dev->PciIo,
                Dev->DmaMapping
                );

  Dev->PciIo->FreeBuffer (
                Dev->PciIo,
                EFI_SIZE_TO_PAGES (sizeof (*Dev->Dma)),
                Dev->Dma
                );

  Dev->PciIo->Attributes (
                Dev->PciIo,
                EfiPciIoAttributeOperationSet,
                Dev->OrigPciAttrs,
                NULL
                );

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

  FreePool (Dev);

  return Status;
}

//
// The static object that groups the Supported() (ie. probe), Start() and
// Stop() functions of the driver together. Refer to UEFI Spec 2.3.1 + Errata
// C, 10.1 EFI Driver Binding Protocol.
//
STATIC
EFI_DRIVER_BINDING_PROTOCOL  gDriverBinding = {
  &LsiScsiControllerSupported,
  &LsiScsiControllerStart,
  &LsiScsiControllerStop,
  0x10, // Version, must be in [0x10 .. 0xFFFFFFEF] for IHV-developed drivers
  NULL, // ImageHandle, to be overwritten by
        // EfiLibInstallDriverBindingComponentName2() in LsiScsiEntryPoint()
  NULL  // DriverBindingHandle, ditto
};

//
// The purpose of the following scaffolding (EFI_COMPONENT_NAME_PROTOCOL and
// EFI_COMPONENT_NAME2_PROTOCOL implementation) is to format the driver's name
// in English, for display on standard console devices. This is recommended for
// UEFI drivers that follow the UEFI Driver Model. Refer to the Driver Writer's
// Guide for UEFI 2.3.1 v1.01, 11 UEFI Driver and Controller Names.
//
// Device type names ("LSI 53C895A SCSI Controller") are not formatted because
// the driver supports only that device type. Therefore the driver name
// suffices for unambiguous identification.
//

STATIC
EFI_UNICODE_STRING_TABLE  mDriverNameTable[] = {
  { "eng;en", L"LSI 53C895A SCSI Controller Driver" },
  { NULL,     NULL                                  }
};

STATIC
EFI_COMPONENT_NAME_PROTOCOL  gComponentName;

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

EFI_STATUS
EFIAPI
LsiScsiGetDeviceName (
  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  gComponentName = {
  &LsiScsiGetDriverName,
  &LsiScsiGetDeviceName,
  "eng" // SupportedLanguages, ISO 639-2 language codes
};

STATIC
EFI_COMPONENT_NAME2_PROTOCOL  gComponentName2 = {
  (EFI_COMPONENT_NAME2_GET_DRIVER_NAME)&LsiScsiGetDriverName,
  (EFI_COMPONENT_NAME2_GET_CONTROLLER_NAME)&LsiScsiGetDeviceName,
  "en" // SupportedLanguages, RFC 4646 language codes
};

//
// Entry point of this driver
//
EFI_STATUS
EFIAPI
LsiScsiEntryPoint (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  return EfiLibInstallDriverBindingComponentName2 (
           ImageHandle,
           SystemTable,
           &gDriverBinding,
           ImageHandle, // The handle to install onto
           &gComponentName,
           &gComponentName2
           );
}
