/** @file
  BlockIo implementation for Xen PV Block driver.

  This file is implementing the interface between the actual driver in
  BlockFront.c to the BlockIo protocol.

  Copyright (C) 2014, Citrix Ltd.

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

**/

#include "XenPvBlkDxe.h"

#include "BlockFront.h"

///
/// Block I/O Media structure
///
GLOBAL_REMOVE_IF_UNREFERENCED
EFI_BLOCK_IO_MEDIA  gXenPvBlkDxeBlockIoMedia = {
  0,      // MediaId
  FALSE,  // RemovableMedia
  FALSE,  // MediaPresent
  FALSE,  // LogicalPartition
  TRUE,   // ReadOnly
  FALSE,  // WriteCaching
  512,    // BlockSize
  512,    // IoAlign, BlockFront does not support less than 512 bits-aligned.
  0,      // LastBlock
  0,      // LowestAlignedLba
  0,      // LogicalBlocksPerPhysicalBlock
  0       // OptimalTransferLengthGranularity
};

///
/// Block I/O Protocol instance
///
GLOBAL_REMOVE_IF_UNREFERENCED
EFI_BLOCK_IO_PROTOCOL  gXenPvBlkDxeBlockIo = {
  EFI_BLOCK_IO_PROTOCOL_REVISION3,          // Revision
  &gXenPvBlkDxeBlockIoMedia,                // Media
  XenPvBlkDxeBlockIoReset,                  // Reset
  XenPvBlkDxeBlockIoReadBlocks,             // ReadBlocks
  XenPvBlkDxeBlockIoWriteBlocks,            // WriteBlocks
  XenPvBlkDxeBlockIoFlushBlocks             // FlushBlocks
};

/**
  Read/Write BufferSize bytes from Lba into Buffer.

  This function is common to XenPvBlkDxeBlockIoReadBlocks and
  XenPvBlkDxeBlockIoWriteBlocks.

  @param  This       Indicates a pointer to the calling context.
  @param  MediaId    Id of the media, changes every time the media is replaced.
  @param  Lba        The starting Logical Block Address to read from/write to.
  @param  BufferSize Size of Buffer, must be a multiple of device block size.
  @param  Buffer     A pointer to the destination/source buffer for the data.
  @param  IsWrite    Indicate if the operation is write or read.

  @return See description of XenPvBlkDxeBlockIoReadBlocks and
          XenPvBlkDxeBlockIoWriteBlocks.
**/
STATIC
EFI_STATUS
XenPvBlkDxeBlockIoReadWriteBlocks (
  IN     EFI_BLOCK_IO_PROTOCOL  *This,
  IN     UINT32                 MediaId,
  IN     EFI_LBA                Lba,
  IN     UINTN                  BufferSize,
  IN OUT VOID                   *Buffer,
  IN     BOOLEAN                IsWrite
  )
{
  XEN_BLOCK_FRONT_IO  IoData;
  EFI_BLOCK_IO_MEDIA  *Media = This->Media;
  UINTN               Sector;
  EFI_STATUS          Status;

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

  if (BufferSize == 0) {
    return EFI_SUCCESS;
  }

  if (BufferSize % Media->BlockSize != 0) {
    DEBUG ((
      DEBUG_ERROR,
      "XenPvBlkDxe: Bad buffer size: 0x%Lx\n",
      (UINT64)BufferSize
      ));
    return EFI_BAD_BUFFER_SIZE;
  }

  if ((Lba > Media->LastBlock) ||
      ((BufferSize / Media->BlockSize) - 1 > Media->LastBlock - Lba))
  {
    DEBUG ((
      DEBUG_ERROR,
      "XenPvBlkDxe: %a with invalid LBA: 0x%Lx, size: 0x%Lx\n",
      IsWrite ? "Write" : "Read",
      Lba,
      (UINT64)BufferSize
      ));
    return EFI_INVALID_PARAMETER;
  }

  if (IsWrite && Media->ReadOnly) {
    return EFI_WRITE_PROTECTED;
  }

  if ((Media->IoAlign > 1) && (UINTN)Buffer & (Media->IoAlign - 1)) {
    //
    // Grub2 does not appear to respect IoAlign of 512, so reallocate the
    // buffer here.
    //
    VOID  *NewBuffer;

    //
    // Try again with a properly aligned buffer.
    //
    NewBuffer = AllocateAlignedPages (
                  (BufferSize + EFI_PAGE_SIZE) / EFI_PAGE_SIZE,
                  Media->IoAlign
                  );
    if (!IsWrite) {
      Status = XenPvBlkDxeBlockIoReadBlocks (
                 This,
                 MediaId,
                 Lba,
                 BufferSize,
                 NewBuffer
                 );
      CopyMem (Buffer, NewBuffer, BufferSize);
    } else {
      CopyMem (NewBuffer, Buffer, BufferSize);
      Status = XenPvBlkDxeBlockIoWriteBlocks (
                 This,
                 MediaId,
                 Lba,
                 BufferSize,
                 NewBuffer
                 );
    }

    FreeAlignedPages (NewBuffer, (BufferSize + EFI_PAGE_SIZE) / EFI_PAGE_SIZE);
    return Status;
  }

  IoData.Dev = XEN_BLOCK_FRONT_FROM_BLOCK_IO (This);
  Sector     = (UINTN)MultU64x32 (Lba, Media->BlockSize / 512);

  while (BufferSize > 0) {
    if (((UINTN)Buffer & EFI_PAGE_MASK) == 0) {
      IoData.Size = MIN (
                      BLKIF_MAX_SEGMENTS_PER_REQUEST * EFI_PAGE_SIZE,
                      BufferSize
                      );
    } else {
      IoData.Size = MIN (
                      (BLKIF_MAX_SEGMENTS_PER_REQUEST - 1) * EFI_PAGE_SIZE,
                      BufferSize
                      );
    }

    IoData.Buffer = Buffer;
    IoData.Sector = Sector;
    BufferSize   -= IoData.Size;
    Buffer        = (VOID *)((UINTN)Buffer + IoData.Size);
    Sector       += IoData.Size / 512;
    Status        = XenPvBlockIo (&IoData, IsWrite);
    if (EFI_ERROR (Status)) {
      DEBUG ((
        DEBUG_ERROR,
        "XenPvBlkDxe: Error during %a operation.\n",
        IsWrite ? "write" : "read"
        ));
      return Status;
    }
  }

  return EFI_SUCCESS;
}

/**
  Read BufferSize bytes from Lba into Buffer.

  @param  This       Indicates a pointer to the calling context.
  @param  MediaId    Id of the media, changes every time the media is replaced.
  @param  Lba        The starting Logical Block Address to read from
  @param  BufferSize Size of Buffer, must be a multiple of device block size.
  @param  Buffer     A pointer to the destination buffer for the data. The caller is
                     responsible for either having implicit or explicit ownership of the buffer.

  @retval EFI_SUCCESS           The data was read correctly from the device.
  @retval EFI_DEVICE_ERROR      The device reported an error while performing the read.
  @retval EFI_NO_MEDIA          There is no media in the device.
  @retval EFI_MEDIA_CHANGED     The MediaId does not match the current device.
  @retval EFI_BAD_BUFFER_SIZE   The Buffer was not a multiple of the block size of the device.
  @retval EFI_INVALID_PARAMETER The read request contains LBAs that are not valid,
                                or the buffer is not on proper alignment.

**/
EFI_STATUS
EFIAPI
XenPvBlkDxeBlockIoReadBlocks (
  IN  EFI_BLOCK_IO_PROTOCOL  *This,
  IN  UINT32                 MediaId,
  IN  EFI_LBA                Lba,
  IN  UINTN                  BufferSize,
  OUT VOID                   *Buffer
  )
{
  return XenPvBlkDxeBlockIoReadWriteBlocks (
           This,
           MediaId,
           Lba,
           BufferSize,
           Buffer,
           FALSE
           );
}

/**
  Write BufferSize bytes from Lba into Buffer.

  @param  This       Indicates a pointer to the calling context.
  @param  MediaId    The media ID that the write request is for.
  @param  Lba        The starting logical block address to be written. The caller is
                     responsible for writing to only legitimate locations.
  @param  BufferSize Size of Buffer, must be a multiple of device block size.
  @param  Buffer     A pointer to the source buffer for the data.

  @retval EFI_SUCCESS           The data was written correctly to the device.
  @retval EFI_WRITE_PROTECTED   The device can not be written to.
  @retval EFI_DEVICE_ERROR      The device reported an error while performing the write.
  @retval EFI_NO_MEDIA          There is no media in the device.
  @retval EFI_MEDIA_CHANGED     The MediaId does not match the current device.
  @retval EFI_BAD_BUFFER_SIZE   The Buffer was not a multiple of the block size of the device.
  @retval EFI_INVALID_PARAMETER The write request contains LBAs that are not valid,
                                or the buffer is not on proper alignment.

**/
EFI_STATUS
EFIAPI
XenPvBlkDxeBlockIoWriteBlocks (
  IN EFI_BLOCK_IO_PROTOCOL  *This,
  IN UINT32                 MediaId,
  IN EFI_LBA                Lba,
  IN UINTN                  BufferSize,
  IN VOID                   *Buffer
  )
{
  return XenPvBlkDxeBlockIoReadWriteBlocks (
           This,
           MediaId,
           Lba,
           BufferSize,
           Buffer,
           TRUE
           );
}

/**
  Flush the Block Device.

  @param  This              Indicates a pointer to the calling context.

  @retval EFI_SUCCESS       All outstanding data was written to the device
  @retval EFI_DEVICE_ERROR  The device reported an error while writing back the data
  @retval EFI_NO_MEDIA      There is no media in the device.

**/
EFI_STATUS
EFIAPI
XenPvBlkDxeBlockIoFlushBlocks (
  IN EFI_BLOCK_IO_PROTOCOL  *This
  )
{
  XenPvBlockSync (XEN_BLOCK_FRONT_FROM_BLOCK_IO (This));
  return EFI_SUCCESS;
}

/**
  Reset the block device hardware.

  @param[in]  This                 Indicates a pointer to the calling context.
  @param[in]  ExtendedVerification Not used.

  @retval EFI_SUCCESS          The device was reset.

**/
EFI_STATUS
EFIAPI
XenPvBlkDxeBlockIoReset (
  IN EFI_BLOCK_IO_PROTOCOL  *This,
  IN BOOLEAN                ExtendedVerification
  )
{
  //
  // Since the initialization of the devices is done, then the device is
  // working correctly.
  //
  return EFI_SUCCESS;
}
