/** @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",
    __FUNCTION__, 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", __FUNCTION__, 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
           );
}
