/**
  Implement UnitTestBootLib using USB Class Boot option.  This should be
  industry standard and should work on all platforms

  Copyright (c) Microsoft Corporation.<BR>
  SPDX-License-Identifier: BSD-2-Clause-Patent
**/

#include <PiDxe.h>
#include <Library/DebugLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>
#include <Library/UefiBootManagerLib.h>
#include <Library/DevicePathLib.h>
#include <Protocol/DevicePath.h>
#include <Library/MemoryAllocationLib.h>

/**
  Set the boot manager to boot from a specific device on the next boot. This
  should be set only for the next boot and shouldn't require any manual clean up

  @retval EFI_SUCCESS      Boot device for next boot was set.
  @retval EFI_UNSUPPORTED  Setting the boot device for the next boot is not
                           supportted.
  @retval Other            Boot device for next boot can not be set.
**/
EFI_STATUS
EFIAPI
SetBootNextDevice (
  VOID
  )
{
  EFI_STATUS                    Status;
  EFI_BOOT_MANAGER_LOAD_OPTION  NewOption;
  UINT32                        Attributes;
  UINT8                         *OptionalData;
  UINT32                        OptionalDataSize;
  UINT16                        BootNextValue;
  USB_CLASS_DEVICE_PATH         UsbDp;
  EFI_DEVICE_PATH_PROTOCOL      *DpEnd;
  EFI_DEVICE_PATH_PROTOCOL      *Dp;
  BOOLEAN                       NewOptionValid;

  OptionalData     = NULL;
  OptionalDataSize = 0;
  BootNextValue    = 0xABCD;  // this should be a safe number...
  DpEnd            = NULL;
  Dp               = NULL;
  NewOptionValid   = FALSE;

  UsbDp.Header.Length[0] = (UINT8)(sizeof (USB_CLASS_DEVICE_PATH) & 0xff);
  UsbDp.Header.Length[1] = (UINT8)(sizeof (USB_CLASS_DEVICE_PATH) >> 8);
  UsbDp.Header.Type      = MESSAGING_DEVICE_PATH;
  UsbDp.Header.SubType   = MSG_USB_CLASS_DP;
  UsbDp.VendorId         = 0xFFFF;
  UsbDp.ProductId        = 0xFFFF;
  UsbDp.DeviceClass      = 0xFF;
  UsbDp.DeviceSubClass   = 0xFF;
  UsbDp.DeviceProtocol   = 0xFF;

  Attributes = LOAD_OPTION_ACTIVE;

  DpEnd = AppendDevicePathNode (NULL, NULL);
  if (DpEnd == NULL) {
    DEBUG ((DEBUG_ERROR, "%a: Unable to create device path.  DpEnd is NULL.\n", __func__));
    Status = EFI_OUT_OF_RESOURCES;
    goto CLEANUP;
  }

  // @MRT --- Is this memory leak because we lose the old Dp memory
  Dp = AppendDevicePathNode (
         DpEnd,
         (EFI_DEVICE_PATH_PROTOCOL *)&UsbDp
         );
  if (Dp == NULL) {
    DEBUG ((DEBUG_ERROR, "%a: Unable to create device path.  Dp is NULL.\n", __func__));
    Status = EFI_OUT_OF_RESOURCES;
    goto CLEANUP;
  }

  Status = EfiBootManagerInitializeLoadOption (
             &NewOption,
             (UINTN)BootNextValue,
             LoadOptionTypeBoot,
             Attributes,
             L"Generic USB Class Device",
             Dp,
             OptionalData,
             OptionalDataSize
             );
  if (EFI_ERROR (Status)) {
    DEBUG ((DEBUG_ERROR, "%a: Error creating load option.  Status = %r\n", __func__, Status));
    goto CLEANUP;
  }

  NewOptionValid = TRUE;
  DEBUG ((DEBUG_VERBOSE, "%a: Generic USB Class Device boot option created.\n", __func__));
  Status = EfiBootManagerLoadOptionToVariable (&NewOption);
  if (EFI_ERROR (Status)) {
    DEBUG ((DEBUG_ERROR, "%a: Error Saving boot option NV variable. Status = %r\n", __func__, Status));
    goto CLEANUP;
  }

  //
  // Set Boot Next
  //
  Status = gRT->SetVariable (
                  L"BootNext",
                  &gEfiGlobalVariableGuid,
                  (EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS | EFI_VARIABLE_NON_VOLATILE),
                  sizeof (BootNextValue),
                  &(BootNextValue)
                  );

  DEBUG ((DEBUG_VERBOSE, "%a - Set BootNext Status (%r)\n", __func__, Status));

CLEANUP:
  if (Dp != NULL) {
    FreePool (Dp);
  }

  if (DpEnd != NULL) {
    FreePool (DpEnd);
  }

  if (NewOptionValid) {
    EfiBootManagerFreeLoadOption (&NewOption);
  }

  return Status;
}
