/** @file
  This file implement the Variable Protocol for the block device.

  Copyright (c) 2015-2016, Linaro Limited. All rights reserved.
  Copyright (c) 2015-2016, Hisilicon Limited. All rights reserved.

  This program and the accompanying materials
  are licensed and made available under the terms and conditions of the BSD License
  which accompanies this distribution.  The full text of the license may be found at
  http://opensource.org/licenses/bsd-license.php

  THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,
  WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.

**/

#include <Guid/VariableFormat.h>
#include <Guid/SystemNvDataGuid.h>

#include <Library/BaseMemoryLib.h>
#include <Library/CacheMaintenanceLib.h>
#include <Library/DebugLib.h>
#include <Library/DevicePathLib.h>
#include <Library/IoLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/PcdLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiLib.h>

#include "BlockVariableDxe.h"


STATIC EFI_PHYSICAL_ADDRESS      mMapNvStorageVariableBase;

EFI_STATUS
EFIAPI
FvbGetAttributes (
  IN CONST  EFI_FIRMWARE_VOLUME_BLOCK2_PROTOCOL    *This,
  OUT       EFI_FVB_ATTRIBUTES_2                   *Attributes
  )
{
  EFI_FVB_ATTRIBUTES_2 FvbAttributes;
  FvbAttributes = (EFI_FVB_ATTRIBUTES_2) (

      EFI_FVB2_READ_ENABLED_CAP | // Reads may be enabled
      EFI_FVB2_READ_STATUS      | // Reads are currently enabled
      EFI_FVB2_STICKY_WRITE     | // A block erase is required to flip bits into EFI_FVB2_ERASE_POLARITY
      EFI_FVB2_MEMORY_MAPPED    | // It is memory mapped
      EFI_FVB2_ERASE_POLARITY     // After erasure all bits take this value (i.e. '1')

      );
  FvbAttributes |= EFI_FVB2_WRITE_STATUS      | // Writes are currently enabled
                   EFI_FVB2_WRITE_ENABLED_CAP;  // Writes may be enabled

  *Attributes = FvbAttributes;

  DEBUG ((DEBUG_BLKIO, "FvbGetAttributes(0x%X)\n", *Attributes));
  return EFI_SUCCESS;
}

EFI_STATUS
EFIAPI
FvbSetAttributes(
  IN CONST  EFI_FIRMWARE_VOLUME_BLOCK2_PROTOCOL  *This,
  IN OUT    EFI_FVB_ATTRIBUTES_2                 *Attributes
  )
{
  DEBUG ((DEBUG_BLKIO, "FvbSetAttributes(0x%X) is not supported\n",*Attributes));
  return EFI_UNSUPPORTED;
}

EFI_STATUS
EFIAPI
FvbGetPhysicalAddress (
  IN CONST  EFI_FIRMWARE_VOLUME_BLOCK2_PROTOCOL  *This,
  OUT       EFI_PHYSICAL_ADDRESS                 *Address
  )
{
  *Address = mMapNvStorageVariableBase;
  return EFI_SUCCESS;
}

EFI_STATUS
EFIAPI
FvbGetBlockSize (
  IN CONST  EFI_FIRMWARE_VOLUME_BLOCK2_PROTOCOL  *This,
  IN        EFI_LBA                              Lba,
  OUT       UINTN                                *BlockSize,
  OUT       UINTN                                *NumberOfBlocks
  )
{
  BLOCK_VARIABLE_INSTANCE       *Instance;

  Instance = CR (This, BLOCK_VARIABLE_INSTANCE, FvbProtocol, BLOCK_VARIABLE_SIGNATURE);
  *BlockSize = (UINTN) Instance->Media.BlockSize;
  *NumberOfBlocks = PcdGet32 (PcdNvStorageVariableBlockCount);
  return EFI_SUCCESS;
}

EFI_STATUS
EFIAPI
FvbRead (
  IN CONST  EFI_FIRMWARE_VOLUME_BLOCK2_PROTOCOL   *This,
  IN        EFI_LBA                               Lba,
  IN        UINTN                                 Offset,
  IN OUT    UINTN                                 *NumBytes,
  IN OUT    UINT8                                 *Buffer
  )
{
  BLOCK_VARIABLE_INSTANCE       *Instance;
  EFI_BLOCK_IO_PROTOCOL         *BlockIo;
  EFI_STATUS                    Status;
  UINTN                         Bytes;
  VOID                          *DataPtr;

  Instance = CR (This, BLOCK_VARIABLE_INSTANCE, FvbProtocol, BLOCK_VARIABLE_SIGNATURE);
  BlockIo = Instance->BlockIoProtocol;
  Bytes = (Offset + *NumBytes + Instance->Media.BlockSize - 1) / Instance->Media.BlockSize * Instance->Media.BlockSize;
  DataPtr = AllocateZeroPool (Bytes);
  if (DataPtr == NULL) {
    DEBUG ((EFI_D_ERROR, "FvbWrite: failed to allocate buffer.\n"));
    return EFI_BUFFER_TOO_SMALL;
  }
  WriteBackDataCacheRange (DataPtr, Bytes);
  InvalidateDataCacheRange (Buffer, *NumBytes);
  Status = BlockIo->ReadBlocks (BlockIo, BlockIo->Media->MediaId, Instance->StartLba + Lba,
		                Bytes, DataPtr);
  if (EFI_ERROR (Status)) {
    DEBUG ((EFI_D_ERROR, "FvbRead StartLba:%x, Lba:%x, Offset:%x, Status:%x\n",
	    Instance->StartLba, Lba, Offset, Status));
    goto exit;
  }
  CopyMem (Buffer, DataPtr + Offset, *NumBytes);
  WriteBackDataCacheRange (Buffer, *NumBytes);
exit:
  FreePool (DataPtr);
  return Status;
}

EFI_STATUS
EFIAPI
FvbWrite (
  IN CONST  EFI_FIRMWARE_VOLUME_BLOCK2_PROTOCOL   *This,
  IN        EFI_LBA                               Lba,
  IN        UINTN                                 Offset,
  IN OUT    UINTN                                 *NumBytes,
  IN        UINT8                                 *Buffer
  )
{
  BLOCK_VARIABLE_INSTANCE       *Instance;
  EFI_BLOCK_IO_PROTOCOL         *BlockIo;
  EFI_STATUS                    Status;
  UINTN                         Bytes;
  VOID                          *DataPtr;

  Instance = CR (This, BLOCK_VARIABLE_INSTANCE, FvbProtocol, BLOCK_VARIABLE_SIGNATURE);
  BlockIo = Instance->BlockIoProtocol;
  Bytes = (Offset + *NumBytes + Instance->Media.BlockSize - 1) / Instance->Media.BlockSize * Instance->Media.BlockSize;
  DataPtr = AllocateZeroPool (Bytes);
  if (DataPtr == NULL) {
    DEBUG ((EFI_D_ERROR, "FvbWrite: failed to allocate buffer.\n"));
    return EFI_BUFFER_TOO_SMALL;
  }
  WriteBackDataCacheRange (DataPtr, Bytes);
  Status = BlockIo->ReadBlocks (BlockIo, BlockIo->Media->MediaId, Instance->StartLba + Lba,
                                Bytes, DataPtr);
  if (EFI_ERROR (Status)) {
    DEBUG ((EFI_D_ERROR, "FvbWrite: failed on reading blocks.\n"));
    goto exit;
  }
  CopyMem (DataPtr + Offset, Buffer, *NumBytes);
  WriteBackDataCacheRange (DataPtr, Bytes);
  Status = BlockIo->WriteBlocks (BlockIo, BlockIo->Media->MediaId, Instance->StartLba + Lba,
                                 Bytes, DataPtr);
  if (EFI_ERROR (Status)) {
    DEBUG ((EFI_D_ERROR, "FvbWrite StartLba:%x, Lba:%x, Offset:%x, Status:%x\n",
            Instance->StartLba, Lba, Offset, Status));
  }
  // Sometimes the variable isn't flushed into block device if it's the last flush operation.
  // So flush it again.
  Status = BlockIo->WriteBlocks (BlockIo, BlockIo->Media->MediaId, Instance->StartLba + Lba,
                                 Bytes, DataPtr);
  if (EFI_ERROR (Status)) {
    DEBUG ((EFI_D_ERROR, "FvbWrite StartLba:%x, Lba:%x, Offset:%x, Status:%x\n",
            Instance->StartLba, Lba, Offset, Status));
  }
exit:
  FreePool (DataPtr);
  return Status;
}

EFI_STATUS
EFIAPI
FvbEraseBlocks (
  IN CONST EFI_FIRMWARE_VOLUME_BLOCK2_PROTOCOL *This,
  ...
  )
{
  return EFI_SUCCESS;
}

STATIC BLOCK_VARIABLE_INSTANCE   mBlockVariableInstance = {
  .Signature      = BLOCK_VARIABLE_SIGNATURE,
  .Media          = {
    .MediaId                       = 0,
    .RemovableMedia                = FALSE,
    .MediaPresent                  = TRUE,
    .LogicalPartition              = TRUE,
    .ReadOnly                      = FALSE,
    .WriteCaching                  = FALSE,
    .BlockSize                     = 0,
    .IoAlign                       = 4,
    .LastBlock                     = 0,
    .LowestAlignedLba              = 0,
    .LogicalBlocksPerPhysicalBlock = 0,
  },
  .FvbProtocol    = {
    .GetAttributes        = FvbGetAttributes,
    .SetAttributes        = FvbSetAttributes,
    .GetPhysicalAddress   = FvbGetPhysicalAddress,
    .GetBlockSize         = FvbGetBlockSize,
    .Read                 = FvbRead,
    .Write                = FvbWrite,
    .EraseBlocks          = FvbEraseBlocks,
  }
};

EFI_STATUS
ValidateFvHeader (
  IN EFI_FIRMWARE_VOLUME_HEADER  *FwVolHeader
  )
{
  UINT16                      Checksum, TempChecksum;
  VARIABLE_STORE_HEADER       *VariableStoreHeader;
  UINTN                       VariableStoreLength;
  UINTN                       FvLength;

  FvLength = (UINTN) (PcdGet32(PcdFlashNvStorageVariableSize) + PcdGet32(PcdFlashNvStorageFtwWorkingSize) +
      PcdGet32(PcdFlashNvStorageFtwSpareSize));

  //
  // Verify the header revision, header signature, length
  // Length of FvBlock cannot be 2**64-1
  // HeaderLength cannot be an odd number
  //
  if (   (FwVolHeader->Revision  != EFI_FVH_REVISION)
      || (FwVolHeader->Signature != EFI_FVH_SIGNATURE)
      || (FwVolHeader->FvLength  != FvLength)
      )
  {
    DEBUG ((EFI_D_ERROR, "ValidateFvHeader: No Firmware Volume header present\n"));
    return EFI_NOT_FOUND;
  }

  // Check the Firmware Volume Guid
  if( CompareGuid (&FwVolHeader->FileSystemGuid, &gEfiSystemNvDataFvGuid) == FALSE ) {
    DEBUG ((EFI_D_ERROR, "ValidateFvHeader: Firmware Volume Guid non-compatible\n"));
    return EFI_NOT_FOUND;
  }

  // Verify the header checksum
  TempChecksum = FwVolHeader->Checksum;
  FwVolHeader->Checksum = 0;
  Checksum = CalculateSum16((UINT16*)FwVolHeader, FwVolHeader->HeaderLength);
  if (Checksum != TempChecksum) {
    DEBUG ((EFI_D_ERROR, "ValidateFvHeader: FV checksum is invalid (Checksum:0x%X)\n",Checksum));
    return EFI_NOT_FOUND;
  }
  FwVolHeader->Checksum = Checksum;

  VariableStoreHeader = (VARIABLE_STORE_HEADER*)((UINTN)FwVolHeader + FwVolHeader->HeaderLength);

  // Check the Variable Store Guid
  if( CompareGuid (&VariableStoreHeader->Signature, &gEfiVariableGuid) == FALSE ) {
    DEBUG ((EFI_D_ERROR, "ValidateFvHeader: Variable Store Guid non-compatible\n"));
    return EFI_NOT_FOUND;
  }

  VariableStoreLength = PcdGet32 (PcdFlashNvStorageVariableSize) - FwVolHeader->HeaderLength;
  if (VariableStoreHeader->Size != VariableStoreLength) {
    DEBUG ((EFI_D_ERROR, "ValidateFvHeader: Variable Store Length does not match\n"));
    return EFI_NOT_FOUND;
  }

  return EFI_SUCCESS;
}

EFI_STATUS
InitNonVolatileVariableStore (
  IN BLOCK_VARIABLE_INSTANCE      *Instance,
  IN VOID                         *Headers,
  IN UINTN                        HeadersLength
  )
{
  EFI_FIRMWARE_VOLUME_HEADER            *FirmwareVolumeHeader;
  EFI_STATUS                            Status;
  VARIABLE_STORE_HEADER                 *VariableStoreHeader;

  // Check if the size of the area is at least one block size
  ASSERT((PcdGet32(PcdFlashNvStorageVariableSize) > 0) && (PcdGet32(PcdFlashNvStorageVariableSize) / Instance->BlockIoProtocol->Media->BlockSize > 0));
  ASSERT((PcdGet32(PcdFlashNvStorageFtwWorkingSize) > 0) && (PcdGet32(PcdFlashNvStorageFtwWorkingSize) / Instance->BlockIoProtocol->Media->BlockSize > 0));
  ASSERT((PcdGet32(PcdFlashNvStorageFtwSpareSize) > 0) && (PcdGet32(PcdFlashNvStorageFtwSpareSize) / Instance->BlockIoProtocol->Media->BlockSize > 0));

  //
  // EFI_FIRMWARE_VOLUME_HEADER
  //
  FirmwareVolumeHeader = (EFI_FIRMWARE_VOLUME_HEADER *)Headers;
  CopyGuid (&FirmwareVolumeHeader->FileSystemGuid, &gEfiSystemNvDataFvGuid);
  FirmwareVolumeHeader->FvLength =
      PcdGet32(PcdFlashNvStorageVariableSize) +
      PcdGet32(PcdFlashNvStorageFtwWorkingSize) +
      PcdGet32(PcdFlashNvStorageFtwSpareSize);
  FirmwareVolumeHeader->Signature = EFI_FVH_SIGNATURE;
  FirmwareVolumeHeader->Attributes = (EFI_FVB_ATTRIBUTES_2) (
                                            EFI_FVB2_READ_ENABLED_CAP   | // Reads may be enabled
                                            EFI_FVB2_READ_STATUS        | // Reads are currently enabled
                                            EFI_FVB2_STICKY_WRITE       | // A block erase is required to flip bits into EFI_FVB2_ERASE_POLARITY
                                            EFI_FVB2_MEMORY_MAPPED      | // It is memory mapped
                                            EFI_FVB2_ERASE_POLARITY     | // After erasure all bits take this value (i.e. '1')
                                            EFI_FVB2_WRITE_STATUS       | // Writes are currently enabled
                                            EFI_FVB2_WRITE_ENABLED_CAP    // Writes may be enabled
                                        );
  FirmwareVolumeHeader->HeaderLength          = sizeof(EFI_FIRMWARE_VOLUME_HEADER) + sizeof(EFI_FV_BLOCK_MAP_ENTRY);
  FirmwareVolumeHeader->Revision              = EFI_FVH_REVISION;
  FirmwareVolumeHeader->BlockMap[0].NumBlocks = PcdGet32 (PcdNvStorageVariableBlockCount);
  FirmwareVolumeHeader->BlockMap[0].Length    = Instance->BlockIoProtocol->Media->BlockSize;
  // BlockMap Terminator
  FirmwareVolumeHeader->BlockMap[1].NumBlocks = 0;
  FirmwareVolumeHeader->BlockMap[1].Length    = 0;
  FirmwareVolumeHeader->Checksum = 0;
  FirmwareVolumeHeader->Checksum = CalculateSum16 ((UINT16*)FirmwareVolumeHeader, FirmwareVolumeHeader->HeaderLength);

  //
  // VARIABLE_STORE_HEADER
  //
  VariableStoreHeader = (VARIABLE_STORE_HEADER*)((UINTN)FirmwareVolumeHeader + FirmwareVolumeHeader->HeaderLength);
  CopyGuid (&VariableStoreHeader->Signature, &gEfiVariableGuid);
  VariableStoreHeader->Size = PcdGet32(PcdFlashNvStorageVariableSize) - FirmwareVolumeHeader->HeaderLength;
  VariableStoreHeader->Format            = VARIABLE_STORE_FORMATTED;
  VariableStoreHeader->State             = VARIABLE_STORE_HEALTHY;

  Status = FvbWrite (&Instance->FvbProtocol, 0, 0, &HeadersLength, Headers);
  return Status;
}

EFI_STATUS
BlockVariableDxeInitialize (
  IN EFI_HANDLE                   ImageHandle,
  IN EFI_SYSTEM_TABLE             *SystemTable
  )
{
  EFI_HANDLE                      Handle;
  EFI_STATUS                      Status;
  BLOCK_VARIABLE_INSTANCE         *Instance = &mBlockVariableInstance;
  UINT32                          Count;
  EFI_LBA                         Lba;
  UINTN                           NvStorageSize;
  EFI_DEVICE_PATH_PROTOCOL        *NvBlockDevicePath;
  UINT8                           *NvStorageData;
  VOID                            *Headers;
  UINTN                           HeadersLength;

  Instance->Signature = BLOCK_VARIABLE_SIGNATURE;

  HeadersLength = sizeof(EFI_FIRMWARE_VOLUME_HEADER) + sizeof(EFI_FV_BLOCK_MAP_ENTRY) + sizeof(VARIABLE_STORE_HEADER);
  Headers = AllocateZeroPool(HeadersLength);
  if (Headers == NULL) {
    DEBUG ((EFI_D_ERROR, "%a: failed to allocate memory of Headers\n", __func__));
    return EFI_OUT_OF_RESOURCES;
  }

  Lba = (EFI_LBA) PcdGet32 (PcdNvStorageVariableBlockLba);
  Count = PcdGet32 (PcdNvStorageVariableBlockCount);
  Instance->Media.BlockSize = PcdGet32 (PcdNvStorageVariableBlockSize);
  NvStorageSize = Count * Instance->Media.BlockSize;
  Instance->StartLba = Lba;
  HeadersLength = sizeof(EFI_FIRMWARE_VOLUME_HEADER) + sizeof(EFI_FV_BLOCK_MAP_ENTRY) + sizeof(VARIABLE_STORE_HEADER);
  if (NvStorageSize < HeadersLength) {
    return EFI_BAD_BUFFER_SIZE;
  }
  NvStorageData = (UINT8 *) (UINTN) PcdGet32(PcdFlashNvStorageVariableBase);
  mMapNvStorageVariableBase = PcdGet32(PcdFlashNvStorageVariableBase);
  NvBlockDevicePath = &Instance->DevicePath;
  NvBlockDevicePath = ConvertTextToDevicePath ((CHAR16*)FixedPcdGetPtr (PcdNvStorageVariableBlockDevicePath));
  Status = gBS->LocateDevicePath (&gEfiBlockIoProtocolGuid, &NvBlockDevicePath,
                                  &Instance->Handle);
  if (EFI_ERROR (Status)) {
    DEBUG ((EFI_D_ERROR, "Warning: Couldn't locate NVM device (status: %r)\n", Status));
    return EFI_INVALID_PARAMETER;
  }
  Status = gBS->OpenProtocol (
		      Instance->Handle,
                      &gEfiBlockIoProtocolGuid,
		      (VOID **) &Instance->BlockIoProtocol,
                      gImageHandle,
                      NULL,
                      EFI_OPEN_PROTOCOL_GET_PROTOCOL
                      );
  if (EFI_ERROR (Status)) {
    DEBUG ((EFI_D_ERROR, "Warning: Couldn't open NVM device (status: %r)\n", Status));
    return EFI_DEVICE_ERROR;
  }
  WriteBackDataCacheRange (Instance, sizeof(BLOCK_VARIABLE_INSTANCE));

  Handle = NULL;
  Status = gBS->InstallMultipleProtocolInterfaces (
		  &Handle,
		  &gEfiFirmwareVolumeBlockProtocolGuid, &Instance->FvbProtocol,
		  NULL
		  );
  if (EFI_ERROR (Status)) {
    goto exit;
  }

  Status = FvbRead (&Instance->FvbProtocol, 0, 0, &HeadersLength, Headers);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Status = ValidateFvHeader (Headers);
  if (EFI_ERROR (Status)) {
    DEBUG ((EFI_D_ERROR, "%a, Found invalid Fv Header\n", __func__));

    // Erase all the block device that is reserved for variable storage
    Status = FvbEraseBlocks (&Instance->FvbProtocol, (EFI_LBA)0, Count, EFI_LBA_LIST_TERMINATOR);
    if (EFI_ERROR (Status)) {
      goto exit;
    }

    Status = InitNonVolatileVariableStore (Instance, Headers, HeadersLength);
    if (EFI_ERROR (Status)) {
      goto exit;
    }
  }

  if (NvStorageSize > ((EFI_FIRMWARE_VOLUME_HEADER*)Headers)->FvLength) {
    NvStorageSize = ((EFI_FIRMWARE_VOLUME_HEADER*)Headers)->FvLength;
    NvStorageSize = ((NvStorageSize + Instance->Media.BlockSize - 1) / Instance->Media.BlockSize) * Instance->Media.BlockSize;
  }
  Status = FvbRead (&Instance->FvbProtocol, 0, 0, &NvStorageSize, NvStorageData);

exit:
  return Status;
}