/** @file
*
*  Copyright (c) 2012-2014, ARM 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 "BootMonFsInternal.h"

EFIAPI
EFI_STATUS
OpenBootMonFsOpenVolume (
  IN EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *This,
  OUT EFI_FILE_PROTOCOL              **Root
  )
{
  BOOTMON_FS_INSTANCE *Instance;

  Instance = BOOTMON_FS_FROM_FS_THIS (This);
  if (Instance == NULL) {
    return EFI_DEVICE_ERROR;
  }

  *Root = &Instance->RootFile->File;

  return EFI_SUCCESS;
}

UINT32
BootMonFsGetImageLength (
  IN BOOTMON_FS_FILE      *File
  )
{
  UINT32                   Index;
  UINT32                   FileSize;
  LIST_ENTRY              *RegionToFlushLink;
  BOOTMON_FS_FILE_REGION  *Region;

  FileSize = 0;

  // Look at all Flash areas to determine file size
  for (Index = 0; Index < HW_IMAGE_DESCRIPTION_REGION_MAX; Index++) {
    FileSize += File->HwDescription.Region[Index].Size;
  }

  // Add the regions that have not been flushed yet
  for (RegionToFlushLink = GetFirstNode (&File->RegionToFlushLink);
       !IsNull (&File->RegionToFlushLink, RegionToFlushLink);
       RegionToFlushLink = GetNextNode (&File->RegionToFlushLink, RegionToFlushLink)
       )
  {
    Region = (BOOTMON_FS_FILE_REGION*)RegionToFlushLink;
    if (Region->Offset + Region->Size > FileSize) {
      FileSize += Region->Offset + Region->Size;
    }
  }

  return FileSize;
}

UINTN
BootMonFsGetPhysicalSize (
  IN BOOTMON_FS_FILE* File
  )
{
  // Return 0 for files that haven't yet been flushed to media
  if (File->HwDescription.RegionCount == 0) {
    return 0;
  }

  return ((File->HwDescription.BlockEnd - File->HwDescription.BlockStart) + 1 )
          * File->Instance->Media->BlockSize;
}

EFIAPI
EFI_STATUS
BootMonFsSetDirPosition (
  IN EFI_FILE_PROTOCOL  *This,
  IN UINT64             Position
  )
{
  BOOTMON_FS_FILE       *File;

  File = BOOTMON_FS_FILE_FROM_FILE_THIS (This);
  if (File == NULL) {
    return EFI_INVALID_PARAMETER;
  }

  // UEFI Spec section 12.5:
  // "The seek request for nonzero is not valid on open directories."
  if (Position != 0) {
    return EFI_UNSUPPORTED;
  }
  File->Position = Position;

  return EFI_SUCCESS;
}

EFI_STATUS
BootMonFsOpenDirectory (
  OUT EFI_FILE_PROTOCOL **NewHandle,
  IN CHAR16             *FileName,
  IN BOOTMON_FS_INSTANCE *Volume
  )
{
  ASSERT(0);

  return EFI_UNSUPPORTED;
}
EFI_STATUS
GetFileSystemVolumeLabelInfo (
  IN BOOTMON_FS_INSTANCE *Instance,
  IN OUT UINTN          *BufferSize,
  OUT VOID              *Buffer
  )
{
  UINTN                         Size;
  EFI_FILE_SYSTEM_VOLUME_LABEL *Label;
  EFI_STATUS                    Status;

  Label = Buffer;

  // Value returned by StrSize includes null terminator.
  Size = SIZE_OF_EFI_FILE_SYSTEM_VOLUME_LABEL
         + StrSize (Instance->FsInfo.VolumeLabel);

  if (*BufferSize >= Size) {
    CopyMem (&Label->VolumeLabel, &Instance->FsInfo.VolumeLabel, Size);
    Status = EFI_SUCCESS;
  } else {
    Status = EFI_BUFFER_TOO_SMALL;
  }
  *BufferSize = Size;
  return Status;
}

// Helper function that calculates a rough "free space" by:
// - Taking the media size
// - Subtracting the sum of all file sizes
// - Subtracting the block size times the number of files
//    (To account for the blocks containing the HW_IMAGE_INFO
STATIC
UINT64
ComputeFreeSpace (
  IN BOOTMON_FS_INSTANCE *Instance
  )
{
  LIST_ENTRY   *FileLink;
  UINT64        FileSizeSum;
  UINT64        MediaSize;
  UINTN         NumFiles;
  EFI_BLOCK_IO_MEDIA *Media;
  BOOTMON_FS_FILE *File;

  Media = Instance->BlockIo->Media;
  MediaSize = Media->BlockSize * (Media->LastBlock + 1);

  NumFiles = 0;
  FileSizeSum = 0;
  for (FileLink = GetFirstNode (&Instance->RootFile->Link);
         !IsNull (&Instance->RootFile->Link, FileLink);
         FileLink = GetNextNode (&Instance->RootFile->Link, FileLink)
         )
  {
    File = BOOTMON_FS_FILE_FROM_LINK_THIS (FileLink);
    FileSizeSum += BootMonFsGetImageLength (File);

    NumFiles++;
  }

  return MediaSize - (FileSizeSum + (Media->BlockSize + NumFiles));
}

EFI_STATUS
GetFilesystemInfo (
  IN BOOTMON_FS_INSTANCE *Instance,
  IN OUT UINTN          *BufferSize,
  OUT VOID              *Buffer
  )
{
  EFI_STATUS              Status;

  if (*BufferSize >= Instance->FsInfo.Size) {
    Instance->FsInfo.FreeSpace = ComputeFreeSpace (Instance);
    CopyMem (Buffer, &Instance->FsInfo, Instance->FsInfo.Size);
    Status = EFI_SUCCESS;
  } else {
    Status = EFI_BUFFER_TOO_SMALL;
  }

  *BufferSize = Instance->FsInfo.Size;
  return Status;
}

EFI_STATUS
GetFileInfo (
  IN BOOTMON_FS_INSTANCE *Instance,
  IN BOOTMON_FS_FILE     *File,
  IN OUT UINTN           *BufferSize,
  OUT VOID               *Buffer
  )
{
  EFI_FILE_INFO   *Info;
  UINTN           ResultSize;
  UINTN           NameSize;
  UINTN           Index;

  if (File == Instance->RootFile) {
    NameSize = 0;
    ResultSize = SIZE_OF_EFI_FILE_INFO + sizeof (CHAR16);
  } else {
    NameSize   = AsciiStrLen (File->HwDescription.Footer.Filename) + 1;
    ResultSize = SIZE_OF_EFI_FILE_INFO + (NameSize * sizeof (CHAR16));
  }

  if (*BufferSize < ResultSize) {
    *BufferSize = ResultSize;
    return EFI_BUFFER_TOO_SMALL;
  }

  Info = Buffer;

  // Zero out the structure
  ZeroMem (Info, ResultSize);

  // Fill in the structure
  Info->Size = ResultSize;

  if (File == Instance->RootFile) {
    Info->Attribute    = EFI_FILE_READ_ONLY | EFI_FILE_DIRECTORY;
    Info->FileName[0]  = L'\0';
  } else {
    Info->FileSize     = BootMonFsGetImageLength (File);
    Info->PhysicalSize = BootMonFsGetPhysicalSize (File);

    for (Index = 0; Index < NameSize; Index++) {
      Info->FileName[Index] = File->HwDescription.Footer.Filename[Index];
    }
  }

  *BufferSize = ResultSize;

  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
GetBootMonFsFileInfo (
  IN BOOTMON_FS_INSTANCE *Instance,
  IN BOOTMON_FS_FILE     *File,
  IN OUT UINTN           *BufferSize,
  OUT VOID               *Buffer
  )
{
  EFI_STATUS             Status;
  BOOTMON_FS_FILE_INFO   *Info;
  UINTN                  ResultSize;
  UINTN                  Index;

  if (File == Instance->RootFile) {
    Status = EFI_UNSUPPORTED;
  } else {
    ResultSize = SIZE_OF_BOOTMON_FS_FILE_INFO;

    if (*BufferSize < ResultSize) {
      *BufferSize = ResultSize;
      Status = EFI_BUFFER_TOO_SMALL;
    } else {
      Info = Buffer;

      // Zero out the structure
      ZeroMem (Info, ResultSize);

      // Fill in the structure
      Info->Size = ResultSize;

      Info->EntryPoint  = File->HwDescription.EntryPoint;
      Info->RegionCount = File->HwDescription.RegionCount;
      for (Index = 0; Index < File->HwDescription.RegionCount; Index++) {
        Info->Region[Index].LoadAddress = File->HwDescription.Region[Index].LoadAddress;
        Info->Region[Index].Size        = File->HwDescription.Region[Index].Size;
        Info->Region[Index].Offset      = File->HwDescription.Region[Index].Offset;
        Info->Region[Index].Checksum    = File->HwDescription.Region[Index].Checksum;
      }
      *BufferSize = ResultSize;
      Status = EFI_SUCCESS;
    }
  }

  return Status;
}

STATIC
EFI_STATUS
SetFileName (
  IN  BOOTMON_FS_FILE *File,
  IN  CHAR16          *FileNameUnicode
  )
{
  CHAR8                 *FileNameAscii;
  UINT16                 SavedChar;
  UINTN                  FileNameSize;
  BOOTMON_FS_FILE       *SameFile;
  EFI_STATUS             Status;

  // EFI Shell inserts '\' in front of the filename that must be stripped
  if (FileNameUnicode[0] == L'\\') {
    FileNameUnicode++;
  }
  //
  // Convert Unicode into Ascii
  //
  SavedChar = L'\0';
  FileNameSize = StrLen (FileNameUnicode) + 1;
  FileNameAscii = AllocatePool (FileNameSize * sizeof (CHAR8));
  if (FileNameAscii == NULL) {
    return EFI_OUT_OF_RESOURCES;
  }
  // If Unicode string is too long then truncate it.
  if (FileNameSize > MAX_NAME_LENGTH) {
    SavedChar = FileNameUnicode[MAX_NAME_LENGTH - 1];
    FileNameUnicode[MAX_NAME_LENGTH - 1] = L'\0';
  }
  UnicodeStrToAsciiStr (FileNameUnicode, FileNameAscii);
  // If the unicode string was truncated then restore its original content.
  if (SavedChar != L'\0') {
    FileNameUnicode[MAX_NAME_LENGTH - 1] = SavedChar;
  }

  // If we're changing the file name
  if (AsciiStrCmp (FileNameAscii, File->HwDescription.Footer.Filename) == 0) {
    // No change to filename.
    Status = EFI_SUCCESS;
  } else if (!(File->OpenMode & EFI_FILE_MODE_WRITE)) {
    // You can only change the filename if you open the file for write.
    Status = EFI_ACCESS_DENIED;
  } else if (BootMonGetFileFromAsciiFileName (
                File->Instance,
                File->HwDescription.Footer.Filename,
                &SameFile) != EFI_NOT_FOUND) {
    // A file with that name already exists.
    Status = EFI_ACCESS_DENIED;
  } else {
    // OK, change the filename.
    AsciiStrCpy (FileNameAscii, File->HwDescription.Footer.Filename);
    Status = EFI_SUCCESS;
  }

  FreePool (FileNameAscii);
  return Status;
}

// Set the file's size (NB "size", not "physical size"). If the change amounts
// to an increase, simply do a write followed by a flush.
// (This is a helper function for SetFileInfo.)
STATIC
EFI_STATUS
SetFileSize (
  IN BOOTMON_FS_INSTANCE *Instance,
  IN BOOTMON_FS_FILE     *BootMonFsFile,
  IN UINTN                NewSize
  )
{
  UINT64             StoredPosition;
  EFI_STATUS         Status;
  EFI_FILE_PROTOCOL *File;
  CHAR8              Buffer;
  UINTN              BufferSize;
  UINT32             OldSize;

  OldSize = BootMonFsFile->HwDescription.Region[0].Size;

  if (OldSize == NewSize) {
    return EFI_SUCCESS;
  }

  Buffer = 0;
  BufferSize = sizeof (Buffer);

  File = &BootMonFsFile->File;

  if (!(BootMonFsFile->OpenMode & EFI_FILE_MODE_WRITE)) {
    return EFI_ACCESS_DENIED;
  }

  if (NewSize <= OldSize) {
    OldSize = NewSize;
  } else {
    // Increasing a file's size is potentially complicated as it may require
    // moving the image description on media. The simplest way to do it is to
    // seek past the end of the file (which is valid in UEFI) and perform a
    // Write.

    // Save position
    Status = File->GetPosition (File, &StoredPosition);
    if (EFI_ERROR (Status)) {
      return Status;
    }

    Status = File->SetPosition (File, NewSize - 1);
    if (EFI_ERROR (Status)) {
      return Status;
    }
    Status = File->Write (File, &BufferSize, &Buffer);
    if (EFI_ERROR (Status)) {
      return Status;
    }

    // Restore saved position
    Status = File->SetPosition (File, NewSize - 1);
    if (EFI_ERROR (Status)) {
      return Status;
    }

    Status = File->Flush (File);
    if (EFI_ERROR (Status)) {
      return Status;
    }
  }
  return EFI_SUCCESS;
}

EFI_STATUS
SetFileInfo (
  IN BOOTMON_FS_INSTANCE *Instance,
  IN BOOTMON_FS_FILE     *File,
  IN UINTN                BufferSize,
  IN EFI_FILE_INFO       *Info
  )
{
  EFI_STATUS             Status;

  Status = EFI_SUCCESS;

  // Note that a call to this function on a file opened read-only is only
  // invalid if it actually changes fields, so  we don't immediately fail if the
  // OpenMode is wrong.
  // Also note that the only fields supported are filename and size, others are
  // ignored.

  if (File != Instance->RootFile) {
    if (!(File->OpenMode & EFI_FILE_MODE_WRITE)) {
      return EFI_ACCESS_DENIED;
    }

    Status = SetFileName (File, Info->FileName);
    if (EFI_ERROR (Status)) {
      return Status;
    }

    // Update file size
    Status = SetFileSize (Instance, File, Info->FileSize);
    if (EFI_ERROR (Status)) {
      return Status;
    }
  }
  return Status;
}

EFIAPI
EFI_STATUS
BootMonFsGetInfo (
  IN EFI_FILE_PROTOCOL  *This,
  IN EFI_GUID           *InformationType,
  IN OUT UINTN          *BufferSize,
  OUT VOID              *Buffer
  )
{
  EFI_STATUS           Status;
  BOOTMON_FS_FILE     *File;
  BOOTMON_FS_INSTANCE *Instance;

  File = BOOTMON_FS_FILE_FROM_FILE_THIS (This);
  if (File == NULL) {
    return EFI_DEVICE_ERROR;
  }

  Instance = File->Instance;

  // If the instance has not been initialized yet then do it ...
  if (!Instance->Initialized) {
    Status = BootMonFsInitialize (Instance);
  } else {
    Status = EFI_SUCCESS;
  }

  if (!EFI_ERROR (Status)) {
    if (CompareGuid (InformationType, &gEfiFileSystemVolumeLabelInfoIdGuid)
        != 0) {
      Status = GetFileSystemVolumeLabelInfo (Instance, BufferSize, Buffer);
    } else if (CompareGuid (InformationType, &gEfiFileSystemInfoGuid) != 0) {
      Status = GetFilesystemInfo (Instance, BufferSize, Buffer);
    } else if (CompareGuid (InformationType, &gEfiFileInfoGuid) != 0) {
      Status = GetFileInfo (Instance, File, BufferSize, Buffer);
    } else if (CompareGuid (InformationType, &gArmBootMonFsFileInfoGuid) != 0) {
      Status = GetBootMonFsFileInfo (Instance, File, BufferSize, Buffer);
    } else {
      Status = EFI_UNSUPPORTED;
    }
  }

  return Status;
}

EFIAPI
EFI_STATUS
BootMonFsSetInfo (
  IN EFI_FILE_PROTOCOL  *This,
  IN EFI_GUID           *InformationType,
  IN UINTN              BufferSize,
  IN VOID               *Buffer
  )
{
  EFI_STATUS           Status;
  BOOTMON_FS_FILE     *File;
  BOOTMON_FS_INSTANCE *Instance;

  File = BOOTMON_FS_FILE_FROM_FILE_THIS (This);
  if (File == NULL) {
    return EFI_DEVICE_ERROR;
  }

  Instance = File->Instance;

  if (CompareGuid (InformationType, &gEfiFileInfoGuid) != 0) {
    Status = SetFileInfo (Instance, File, BufferSize, (EFI_FILE_INFO *) Buffer);
  } else {
    // The only writable field in the other two information types
    // (i.e. EFI_FILE_SYSTEM_INFO and EFI_FILE_SYSTEM_VOLUME_LABEL) is the
    // filesystem volume label. This can be retrieved with GetInfo, but it is
    // hard-coded into this driver, not stored on media.
    Status = EFI_UNSUPPORTED;
  }

  return Status;
}

EFIAPI
EFI_STATUS
BootMonFsReadDirectory (
  IN EFI_FILE_PROTOCOL    *This,
  IN OUT UINTN            *BufferSize,
  OUT VOID                *Buffer
  )
{
  BOOTMON_FS_INSTANCE *Instance;
  BOOTMON_FS_FILE     *RootFile;
  BOOTMON_FS_FILE     *File;
  EFI_FILE_INFO       *Info;
  UINTN               NameSize;
  UINTN               ResultSize;
  EFI_STATUS          Status;
  UINTN               Index;

  RootFile = BOOTMON_FS_FILE_FROM_FILE_THIS (This);
  if (RootFile == NULL) {
    return EFI_INVALID_PARAMETER;
  }

  Instance = RootFile->Instance;
  Status = BootMonGetFileFromPosition (Instance, RootFile->Position, &File);
  if (EFI_ERROR (Status)) {
    // No more file
    *BufferSize = 0;
    return EFI_SUCCESS;
  }

  NameSize   = AsciiStrLen (File->HwDescription.Footer.Filename) + 1;
  ResultSize = SIZE_OF_EFI_FILE_INFO + (NameSize * sizeof (CHAR16));
  if (*BufferSize < ResultSize) {
    *BufferSize = ResultSize;
    return EFI_BUFFER_TOO_SMALL;
  }

  // Zero out the structure
  Info = Buffer;
  ZeroMem (Info, ResultSize);

  // Fill in the structure
  Info->Size         = ResultSize;
  Info->FileSize     = BootMonFsGetImageLength (File);
  Info->PhysicalSize = BootMonFsGetPhysicalSize (File);
  for (Index = 0; Index < NameSize; Index++) {
    Info->FileName[Index] = File->HwDescription.Footer.Filename[Index];
  }

  *BufferSize = ResultSize;
  RootFile->Position++;

  return EFI_SUCCESS;
}

EFIAPI
EFI_STATUS
BootMonFsFlushDirectory (
  IN EFI_FILE_PROTOCOL  *This
  )
{
  BOOTMON_FS_FILE *RootFile;
  LIST_ENTRY      *ListFiles;
  LIST_ENTRY      *Link;
  BOOTMON_FS_FILE *File;

  RootFile = BOOTMON_FS_FILE_FROM_FILE_THIS (This);
  if (RootFile == NULL) {
    return EFI_INVALID_PARAMETER;
  }

  ListFiles = &RootFile->Link;

  if (IsListEmpty (ListFiles)) {
    return EFI_SUCCESS;
  }

  //
  // Flush all the files that need to be flushed
  //

  // Go through all the list of files to flush them
  for (Link = GetFirstNode (ListFiles);
       !IsNull (ListFiles, Link);
       Link = GetNextNode (ListFiles, Link)
       )
  {
    File = BOOTMON_FS_FILE_FROM_LINK_THIS (Link);
    File->File.Flush (&File->File);
  }

  return EFI_SUCCESS;
}
