/** @file
*
*  Copyright (c) 2012-2014, ARM Limited. All rights reserved.
*
*  SPDX-License-Identifier: BSD-2-Clause-Patent
*
**/

#include <Library/BaseMemoryLib.h>
#include <Library/DevicePathLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/PrintLib.h>
#include <Library/UefiBootServicesTableLib.h>

#include <Protocol/DevicePathFromText.h>
#include <Protocol/DriverBinding.h>

#include "BootMonFsInternal.h"

EFI_DEVICE_PATH* mBootMonFsSupportedDevicePaths;
LIST_ENTRY       mInstances;

EFI_FILE_PROTOCOL mBootMonFsRootTemplate = {
  EFI_FILE_PROTOCOL_REVISION,
  BootMonFsOpenFile,
  BootMonFsCloseFile,
  BootMonFsDeleteFail,
  BootMonFsReadDirectory,
  BootMonFsWriteFile,
  BootMonFsGetPositionUnsupported,  // UEFI Spec: GetPosition not valid on dirs
  BootMonFsSetDirPosition,
  BootMonFsGetInfo,
  BootMonFsSetInfo,
  BootMonFsFlushDirectory
};

EFI_FILE_PROTOCOL mBootMonFsFileTemplate = {
  EFI_FILE_PROTOCOL_REVISION,
  BootMonFsOpenFile,
  BootMonFsCloseFile,
  BootMonFsDelete,
  BootMonFsReadFile,
  BootMonFsWriteFile,
  BootMonFsGetPosition,
  BootMonFsSetPosition,
  BootMonFsGetInfo,
  BootMonFsSetInfo,
  BootMonFsFlushFile
};

/**
  Search for a file given its name coded in Ascii.

  When searching through the files of the volume, if a file is currently not
  open, its name was written on the media and is kept in RAM in the
  "HwDescription.Footer.Filename[]" field of the file's description.

  If a file is currently open, its name might not have been written on the
  media yet, and as the "HwDescription" is a mirror in RAM of what is on the
  media the "HwDescription.Footer.Filename[]" might be outdated. In that case,
  the up to date name of the file is stored in the "Info" field of the file's
  description.

  @param[in]   Instance       Pointer to the description of the volume in which
                              the file has to be search for.
  @param[in]   AsciiFileName  Name of the file.

  @param[out]  File           Pointer to the description of the file if the
                              file was found.

  @retval  EFI_SUCCESS    The file was found.
  @retval  EFI_NOT_FOUND  The file was not found.

**/
EFI_STATUS
BootMonGetFileFromAsciiFileName (
  IN  BOOTMON_FS_INSTANCE   *Instance,
  IN  CHAR8*                AsciiFileName,
  OUT BOOTMON_FS_FILE       **File
  )
{
  LIST_ENTRY       *Entry;
  BOOTMON_FS_FILE  *FileEntry;
  CHAR8            OpenFileAsciiFileName[MAX_NAME_LENGTH];
  CHAR8            *AsciiFileNameToCompare;

  // Go through all the files in the list and return the file handle
  for (Entry = GetFirstNode (&Instance->RootFile->Link);
       !IsNull (&Instance->RootFile->Link, Entry);
       Entry = GetNextNode (&Instance->RootFile->Link, Entry)
       )
  {
    FileEntry = BOOTMON_FS_FILE_FROM_LINK_THIS (Entry);
    if (FileEntry->Info != NULL) {
      UnicodeStrToAsciiStrS (FileEntry->Info->FileName, OpenFileAsciiFileName,
        MAX_NAME_LENGTH);
      AsciiFileNameToCompare = OpenFileAsciiFileName;
    } else {
      AsciiFileNameToCompare = FileEntry->HwDescription.Footer.Filename;
    }

    if (AsciiStrCmp (AsciiFileNameToCompare, AsciiFileName) == 0) {
      *File = FileEntry;
      return EFI_SUCCESS;
    }
  }
  return EFI_NOT_FOUND;
}

EFI_STATUS
BootMonGetFileFromPosition (
  IN  BOOTMON_FS_INSTANCE   *Instance,
  IN  UINTN                 Position,
  OUT BOOTMON_FS_FILE       **File
  )
{
  LIST_ENTRY        *Entry;
  BOOTMON_FS_FILE   *FileEntry;

  // Go through all the files in the list and return the file handle
  for (Entry = GetFirstNode (&Instance->RootFile->Link);
       !IsNull (&Instance->RootFile->Link, Entry) && (&Instance->RootFile->Link != Entry);
       Entry = GetNextNode (&Instance->RootFile->Link, Entry)
       )
  {
    if (Position == 0) {
      FileEntry = BOOTMON_FS_FILE_FROM_LINK_THIS (Entry);
      *File = FileEntry;
      return EFI_SUCCESS;
    }
    Position--;
  }
  return EFI_NOT_FOUND;
}

EFI_STATUS
BootMonFsCreateFile (
  IN  BOOTMON_FS_INSTANCE *Instance,
  OUT BOOTMON_FS_FILE     **File
  )
{
  BOOTMON_FS_FILE *NewFile;

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

  NewFile->Signature = BOOTMON_FS_FILE_SIGNATURE;
  InitializeListHead (&NewFile->Link);
  InitializeListHead (&NewFile->RegionToFlushLink);
  NewFile->Instance = Instance;

  // If the created file is the root file then create a directory EFI_FILE_PROTOCOL
  if (Instance->RootFile == *File) {
    CopyMem (&NewFile->File, &mBootMonFsRootTemplate, sizeof (mBootMonFsRootTemplate));
  } else {
    CopyMem (&NewFile->File, &mBootMonFsFileTemplate, sizeof (mBootMonFsFileTemplate));
  }
  *File = NewFile;
  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
SupportedDevicePathsInit (
  VOID
  )
{
  EFI_STATUS                          Status;
  CHAR16*                             DevicePathListStr;
  CHAR16*                             DevicePathStr;
  CHAR16*                             NextDevicePathStr;
  EFI_DEVICE_PATH_FROM_TEXT_PROTOCOL *EfiDevicePathFromTextProtocol;
  EFI_DEVICE_PATH_PROTOCOL           *Instance;

  Status = gBS->LocateProtocol (&gEfiDevicePathFromTextProtocolGuid, NULL, (VOID **)&EfiDevicePathFromTextProtocol);
  ASSERT_EFI_ERROR (Status);

  // Initialize Variable
  DevicePathListStr = (CHAR16*)PcdGetPtr (PcdBootMonFsSupportedDevicePaths);
  mBootMonFsSupportedDevicePaths = NULL;

  // Extract the Device Path instances from the multi-device path string
  while ((DevicePathListStr != NULL) && (DevicePathListStr[0] != L'\0')) {
    NextDevicePathStr = StrStr (DevicePathListStr, L";");
    if (NextDevicePathStr == NULL) {
      DevicePathStr = DevicePathListStr;
      DevicePathListStr = NULL;
    } else {
      DevicePathStr = (CHAR16*)AllocateCopyPool ((NextDevicePathStr - DevicePathListStr + 1) * sizeof (CHAR16), DevicePathListStr);
      if (DevicePathStr == NULL) {
        return EFI_OUT_OF_RESOURCES;
      }
      *(DevicePathStr + (NextDevicePathStr - DevicePathListStr)) = L'\0';
      DevicePathListStr = NextDevicePathStr;
      if (DevicePathListStr[0] == L';') {
        DevicePathListStr++;
      }
    }

    Instance = EfiDevicePathFromTextProtocol->ConvertTextToDevicePath (DevicePathStr);
    ASSERT (Instance != NULL);
    mBootMonFsSupportedDevicePaths = AppendDevicePathInstance (mBootMonFsSupportedDevicePaths, Instance);

    if (NextDevicePathStr != NULL) {
      FreePool (DevicePathStr);
    }
    FreePool (Instance);
  }

  if (mBootMonFsSupportedDevicePaths == NULL) {
    return EFI_UNSUPPORTED;
  } else {
    return EFI_SUCCESS;
  }
}

EFI_STATUS
EFIAPI
BootMonFsDriverSupported (
  IN        EFI_DRIVER_BINDING_PROTOCOL *DriverBinding,
  IN        EFI_HANDLE                   ControllerHandle,
  IN        EFI_DEVICE_PATH_PROTOCOL    *DevicePath OPTIONAL
  )
{
  EFI_DISK_IO_PROTOCOL  *DiskIo;
  EFI_DEVICE_PATH_PROTOCOL *DevicePathProtocol;
  EFI_DEVICE_PATH_PROTOCOL *SupportedDevicePath;
  EFI_DEVICE_PATH_PROTOCOL *SupportedDevicePaths;
  EFI_STATUS Status;
  UINTN Size1;
  UINTN Size2;

  //
  // Open the IO Abstraction(s) needed to perform the supported test
  //
  Status = gBS->OpenProtocol (
                  ControllerHandle,
                  &gEfiDiskIoProtocolGuid,
                  (VOID **) &DiskIo,
                  gImageHandle,
                  ControllerHandle,
                  EFI_OPEN_PROTOCOL_BY_DRIVER
                  );

  if (EFI_ERROR (Status)) {
    return Status;
  }
  //
  // Close the I/O Abstraction(s) used to perform the supported test
  //
  gBS->CloseProtocol (
         ControllerHandle,
         &gEfiDiskIoProtocolGuid,
         gImageHandle,
         ControllerHandle
         );

  // Check that BlockIo protocol instance exists
  Status = gBS->OpenProtocol (
                  ControllerHandle,
                  &gEfiBlockIoProtocolGuid,
                  NULL,
                  gImageHandle,
                  ControllerHandle,
                  EFI_OPEN_PROTOCOL_TEST_PROTOCOL
                  );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  // Check if a DevicePath is attached to the handle
  Status = gBS->OpenProtocol (
                  ControllerHandle,
                  &gEfiDevicePathProtocolGuid,
                  (VOID **)&DevicePathProtocol,
                  gImageHandle,
                  ControllerHandle,
                  EFI_OPEN_PROTOCOL_BY_DRIVER
                  );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  // Check if the Device Path is the one which contains the Boot Monitor File System
  Size1 = GetDevicePathSize (DevicePathProtocol);

  // Go through the list of Device Path Instances
  Status = EFI_UNSUPPORTED;
  SupportedDevicePaths = mBootMonFsSupportedDevicePaths;
  while (SupportedDevicePaths != NULL) {
    SupportedDevicePath = GetNextDevicePathInstance (&SupportedDevicePaths, &Size2);

    if ((Size1 == Size2) && (CompareMem (DevicePathProtocol, SupportedDevicePath, Size1) == 0)) {
      // The Device Path is supported
      Status = EFI_SUCCESS;
      break;
    }
  }

  gBS->CloseProtocol (ControllerHandle, &gEfiDevicePathProtocolGuid, gImageHandle, ControllerHandle);
  return Status;
}

EFI_STATUS
EFIAPI
BootMonFsDriverStart (
  IN        EFI_DRIVER_BINDING_PROTOCOL *DriverBinding,
  IN        EFI_HANDLE                   ControllerHandle,
  IN        EFI_DEVICE_PATH_PROTOCOL    *DevicePath OPTIONAL
  )
{
  BOOTMON_FS_INSTANCE *Instance;
  EFI_STATUS           Status;
  UINTN                VolumeNameSize;
  EFI_FILE_INFO       *Info;

  Instance = AllocateZeroPool (sizeof (BOOTMON_FS_INSTANCE));
  if (Instance == NULL) {
    return EFI_OUT_OF_RESOURCES;
  }

  // Initialize the BlockIo of the Instance
  Status = gBS->OpenProtocol (
                  ControllerHandle,
                  &gEfiBlockIoProtocolGuid,
                  (VOID **)&(Instance->BlockIo),
                  gImageHandle,
                  ControllerHandle,
                  EFI_OPEN_PROTOCOL_GET_PROTOCOL
                  );
  if (EFI_ERROR (Status)) {
    goto Error;
  }

  Status = gBS->OpenProtocol (
                  ControllerHandle,
                  &gEfiDiskIoProtocolGuid,
                  (VOID **)&(Instance->DiskIo),
                  gImageHandle,
                  ControllerHandle,
                  EFI_OPEN_PROTOCOL_BY_DRIVER
                  );
  if (EFI_ERROR (Status)) {
    goto Error;
  }

  //
  // Initialize the attributes of the Instance
  //
  Instance->Signature = BOOTMON_FS_SIGNATURE;
  Instance->ControllerHandle = ControllerHandle;
  Instance->Media = Instance->BlockIo->Media;
  Instance->Binding = DriverBinding;

    // Initialize the Simple File System Protocol
  Instance->Fs.Revision = EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_REVISION;
  Instance->Fs.OpenVolume = OpenBootMonFsOpenVolume;

  // Volume name + L' ' + '2' digit number
  VolumeNameSize = StrSize (BOOTMON_FS_VOLUME_LABEL) + (3 * sizeof (CHAR16));

  // Initialize FileSystem Information
  Instance->FsInfo.Size = SIZE_OF_EFI_FILE_SYSTEM_INFO + VolumeNameSize;
  Instance->FsInfo.BlockSize = Instance->Media->BlockSize;
  Instance->FsInfo.ReadOnly = FALSE;
  Instance->FsInfo.VolumeSize =
    Instance->Media->BlockSize * (Instance->Media->LastBlock - Instance->Media->LowestAlignedLba);
  CopyMem (Instance->FsInfo.VolumeLabel, BOOTMON_FS_VOLUME_LABEL, StrSize (BOOTMON_FS_VOLUME_LABEL));

  // Initialize the root file
  Status = BootMonFsCreateFile (Instance, &Instance->RootFile);
  if (EFI_ERROR (Status)) {
    goto Error;
  }

  Info = AllocateZeroPool (sizeof (EFI_FILE_INFO));
  if (Info == NULL) {
    Status = EFI_OUT_OF_RESOURCES;
    goto Error;
  }
  Instance->RootFile->Info = Info;

  // Initialize the DevicePath of the Instance
  Status = gBS->OpenProtocol (
                  ControllerHandle,
                  &gEfiDevicePathProtocolGuid,
                  (VOID **)&(Instance->DevicePath),
                  gImageHandle,
                  ControllerHandle,
                  EFI_OPEN_PROTOCOL_GET_PROTOCOL
                  );
  if (EFI_ERROR (Status)) {
    goto Error;
  }

  //
  // Install the Simple File System Protocol
  //
  Status = gBS->InstallMultipleProtocolInterfaces (
                      &ControllerHandle,
                      &gEfiSimpleFileSystemProtocolGuid, &Instance->Fs,
                      NULL
                      );
  if (EFI_ERROR (Status)) {
    goto Error;
  }

  InsertTailList (&mInstances, &Instance->Link);

  return EFI_SUCCESS;

Error:

    if (Instance->RootFile != NULL) {
      if (Instance->RootFile->Info != NULL) {
        FreePool (Instance->RootFile->Info);
      }
      FreePool (Instance->RootFile);
    }
    FreePool (Instance);

  return Status;
}


EFI_STATUS
EFIAPI
BootMonFsDriverStop (
  IN        EFI_DRIVER_BINDING_PROTOCOL *DriverBinding,
  IN        EFI_HANDLE                   ControllerHandle,
  IN        UINTN                        NumberOfChildren,
  IN        EFI_HANDLE                  *ChildHandleBuffer OPTIONAL
  )
{
  BOOTMON_FS_INSTANCE *Instance;
  LIST_ENTRY          *Link;
  EFI_STATUS           Status;
  BOOLEAN              InstanceFound;

  // Find instance from ControllerHandle.
  Instance = NULL;
  InstanceFound = FALSE;
  // For each instance in mInstances:
  for (Link = GetFirstNode (&mInstances); !IsNull (&mInstances, Link); Link = GetNextNode (&mInstances, Link)) {
    Instance = BOOTMON_FS_FROM_LINK (Link);

    if (Instance->ControllerHandle == ControllerHandle) {
      InstanceFound = TRUE;
      break;
    }
  }
  ASSERT (InstanceFound == TRUE);

  gBS->CloseProtocol (
      ControllerHandle,
      &gEfiDevicePathProtocolGuid,
      DriverBinding->ImageHandle,
      ControllerHandle);

  gBS->CloseProtocol (
      ControllerHandle,
      &gEfiDiskIoProtocolGuid,
      DriverBinding->ImageHandle,
      ControllerHandle);

  gBS->CloseProtocol (
      ControllerHandle,
      &gEfiBlockIoProtocolGuid,
      DriverBinding->ImageHandle,
      ControllerHandle);

  Status = gBS->UninstallMultipleProtocolInterfaces (
      &ControllerHandle,
      &gEfiSimpleFileSystemProtocolGuid, &Instance->Fs,
      NULL);

  FreePool (Instance->RootFile->Info);
  FreePool (Instance->RootFile);
  FreePool (Instance);

  return Status;
}

//
// Simple Network Protocol Driver Global Variables
//
EFI_DRIVER_BINDING_PROTOCOL mBootMonFsDriverBinding = {
  BootMonFsDriverSupported,
  BootMonFsDriverStart,
  BootMonFsDriverStop,
  0xa,
  NULL,
  NULL
};

EFI_STATUS
EFIAPI
BootMonFsEntryPoint (
  IN EFI_HANDLE                            ImageHandle,
  IN EFI_SYSTEM_TABLE                      *SystemTable
  )
{
  EFI_STATUS  Status;

  InitializeListHead (&mInstances);

  // Initialize the list of Device Paths that could support BootMonFs
  Status = SupportedDevicePathsInit ();
  if (!EFI_ERROR (Status)) {
    Status = gBS->InstallMultipleProtocolInterfaces (
                    &ImageHandle,
                    &gEfiDriverBindingProtocolGuid, &mBootMonFsDriverBinding,
                    NULL
                    );
    ASSERT_EFI_ERROR (Status);
  } else {
    DEBUG((EFI_D_ERROR,"Warning: No Device Paths supporting BootMonFs have been defined in the PCD.\n"));
  }

  return Status;
}
