/** @file

Copyright (c) 2021, American Megatrends International LLC. All rights reserved.<BR>
SPDX-License-Identifier: BSD-2-Clause-Patent

**/


#include <PiDxe.h>
#include <Uefi.h>
#include <Library/BaseLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/DebugLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Guid/DxeServices.h>
#include <Library/DxeServicesTableLib.h>
#include <Library/UefiLib.h>
#include <Protocol/FirmwareVolume2.h>
#include <Protocol/LoadedImage.h>
#include <Guid/MemoryTypeInformation.h>
#include <Pi/PiDxeCis.h>
#include <Pi/PiHob.h>
#include <Library/PcdLib.h>
#include <Library/DxeServicesLib.h>
#include "LinuxBoot.h"

#define LINUX_BOOT_KERNEL_GUID \
  { \
    0x81339b04, 0xfa8c, 0x4be0, {0x9c, 0xa7, 0x91, 0x6f, 0xc5, 0x31, 0x9e, 0xb5} \
  }


EFI_STATUS
EFIAPI
LoadLinuxCheckKernelSetup (
  IN VOID        *KernelSetup,
  IN UINTN       KernelSetupSize
  );

VOID*
EFIAPI
LoadLinuxAllocateKernelSetupPages (
  IN UINTN                  Pages
  );

EFI_STATUS
EFIAPI
LoadLinuxInitializeKernelSetup (
  IN VOID        *KernelSetup
  );

VOID*
EFIAPI
LoadLinuxAllocateKernelPages (
  IN VOID                   *KernelSetup,
  IN UINTN                  Pages
  );

EFI_STATUS
EFIAPI
LoadLinuxSetCommandLine (
  IN OUT VOID    *KernelSetup,
  IN CHAR8       *CommandLine
  );

EFI_STATUS
EFIAPI
LoadLinux (
  IN VOID      *Kernel,
  IN OUT VOID  *KernelSetup
  );

EFI_GUID gLinuxBootKernelFileGuid = LINUX_BOOT_KERNEL_GUID;

//---------------------------------------------------------------------------

/**
  Dump some hexadecimal data to the screen.

  @note Function taken from ShellPkg/Library/UefiShellCommandLib/UefiShellCommandLib.c in EDKII

  @param[in] Indent     How many spaces to indent the output.
  @param[in] Offset     The offset of the printing.
  @param[in] DataSize   The size in bytes of UserData.
  @param[in] UserData   The data to print out.
**/
static
VOID
DumpHex (
  IN UINTN        Indent,
  IN UINTN        Offset,
  IN UINTN        DataSize,
  IN VOID         *UserData
  )
{
  UINT8 *Data;
  CHAR8 Val[50];
  CHAR8 Str[20];
  UINT8 TempByte;
  UINTN Size;
  UINTN Index;
  CHAR8 Hex[] = {
    '0', '1', '2', '3', '4', '5', '6', '7',
    '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
  };

  DEBUG((DEBUG_INFO, "%*a          00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F\n", Indent, ""));
  DEBUG((DEBUG_INFO, "%*a          ------------------------------------------------\n", Indent, ""));

  Data = UserData;
  while (DataSize != 0) {
    Size = 16;
    if (Size > DataSize) {
      Size = DataSize;
    }

    for (Index = 0; Index < Size; Index += 1) {
      TempByte            = Data[Index];
      Val[Index * 3 + 0]  = Hex[TempByte >> 4];
      Val[Index * 3 + 1]  = Hex[TempByte & 0xF];
      Val[Index * 3 + 2]  = (CHAR8) ((Index == 7) ? '-' : ' ');
      Str[Index]          = (CHAR8) ((TempByte < ' ' || TempByte > 'z') ? '.' : TempByte);
    }

    Val[Index * 3]  = 0;
    Str[Index]      = 0;
    DEBUG((DEBUG_INFO, "%*a%08X: %-48a  %a\n", Indent, "", Offset, Val, Str));

    Data += Size;
    Offset += Size;
    DataSize -= Size;
  }
}


/**
 *  This function completes a minimal amount of the necessary BDS functions to prepare
 *  for booting the kernel.
 *
 *  @param  None
 *
 *  @retval EFI_SUCCESS         Successfully completed remaining tasks
 *  @return EFI_ERROR           Could not complete BDS tasks
 */
EFI_STATUS
CompleteBdsTasks (
    VOID
)
{

    return EFI_SUCCESS;
}

/**
 *  This function will load and launch the Linux kernel from a BIOS FV.
 *
 *  @note   This function is not intended to return. Any exiting from this function indicates
 *          a problem loading or launching the kernel.
 *
 *  @param  None
 *
 *  @return EFI_ERROR           Any error code
 */
EFI_STATUS
LoadAndLaunchKernel (
    VOID
)
{
    EFI_LOADED_IMAGE_PROTOCOL   *LoadedImage = NULL;
    EFI_STATUS                  Status;
    EFI_HANDLE                  KernelHandle = NULL;
    VOID                        *KernelBuffer = NULL;
    VOID                        *KernelFfsBuffer = NULL;
    UINTN                       KernelFfsSize = 0;
    struct BootParams          *BootParams = NULL;
    struct BootParams          *HandoverParams = NULL;
    UINT32                      StartOffset = 0;
    UINT32                      KernelLength = 0;
    UINT8                       *Temp;
    UINT8                       CmdLine[] = " ";

    DEBUG((DEBUG_INFO, "LoadAndLaunchKernel Entry\n"));

    ///
    /// Kernel load and preparation
    ///
    DEBUG((DEBUG_INFO, "Preparing the kernel...\n"));

    // Retrieve the kernel from the firmware volume
    Status = GetSectionFromAnyFv(
        &gLinuxBootKernelFileGuid,
        EFI_SECTION_PE32,
        0,
        &KernelFfsBuffer,
        &KernelFfsSize
    );

    DEBUG((DEBUG_INFO, "Status %r\n",Status));
    DEBUG((DEBUG_INFO, "KernelFfsBuffer %x\n",KernelFfsBuffer));
    DEBUG((DEBUG_INFO, "KernelFfsSize %x\n",KernelFfsSize));

    if (EFI_ERROR(Status)) {
        DEBUG((DEBUG_ERROR, "Could not retrieve kernel; %r.\n", Status));
        goto FatalError;
    }

    DEBUG((DEBUG_INFO, "Loaded kernel to buffer at 0x%p with size 0x%X.\n", KernelFfsBuffer, KernelFfsSize));
    DEBUG((DEBUG_INFO, "Printing first 0x%X bytes:\n", MIN(KernelFfsSize, 0x100)));

    DumpHex(2, 0, MIN(0x100, KernelFfsSize), KernelFfsBuffer);

    // Create a LoadImage protocol for the kernel
    Status = gBS->LoadImage(TRUE, gImageHandle, NULL, KernelFfsBuffer, KernelFfsSize, &KernelHandle);
    if (EFI_ERROR(Status)) {
        DEBUG((DEBUG_ERROR, "Could not create LoadImage for kernel %r\n", Status));
        goto FatalError;
    }

    // Get the new LoadedImage protocol to retrieve information about the kernel
    Status = gBS->HandleProtocol(KernelHandle, &gEfiLoadedImageProtocolGuid, (VOID **) &LoadedImage);
    if (EFI_ERROR(Status)) {
        DEBUG((DEBUG_ERROR, "Could not get kernel LoadedImage protocol; %r\n", Status));
        goto FatalError;
    }

    DEBUG((DEBUG_INFO, "Kernel LoadedImage information:\n"));
    DEBUG((DEBUG_INFO, "  ImageBase = 0x%p\n", LoadedImage->ImageBase));
    DEBUG((DEBUG_INFO, "  ImageSize = 0x%p\n", LoadedImage->ImageSize));

    // Verify the kernel boot parameters from the LoadedImage and allocate an initalization buffer once verified
    BootParams = (struct BootParams*) LoadedImage->ImageBase;

    Status = LoadLinuxCheckKernelSetup((VOID *) BootParams, sizeof(struct BootParams));
    if (EFI_ERROR (Status)) {
        DEBUG((DEBUG_ERROR, "LoadLinuxCheckKernelSetup failed; %r.\n", Status));
        goto FatalError;
    }

    HandoverParams = LoadLinuxAllocateKernelSetupPages(EFI_SIZE_TO_PAGES(KERNEL_SETUP_SIZE));
    if (HandoverParams == NULL) {
        DEBUG((DEBUG_ERROR, "Could not allocate memory for kernel handover parameters.\n"));
        goto FatalError;
    }
    DEBUG((DEBUG_INFO, "Handover parameters allocated at 0x%p\n", HandoverParams));

    gBS->CopyMem(&HandoverParams->Hdr, &BootParams->Hdr, sizeof(struct SetupHeader));

    Status = LoadLinuxInitializeKernelSetup(HandoverParams);
    if (EFI_ERROR (Status)) {
        DEBUG((DEBUG_ERROR, "Unable to initialize the handover parameters; %r.\n", Status));
        goto FatalError;
    }

    // Allocate space for the kernel and copy it into the new buffer
    KernelBuffer = LoadLinuxAllocateKernelPages(HandoverParams, EFI_SIZE_TO_PAGES(HandoverParams->Hdr.InitSize));
    if (KernelBuffer == NULL) {
        DEBUG((DEBUG_ERROR, "Unable to allocate memory for kernel.\n"));
        goto FatalError;
    }

    StartOffset = (HandoverParams->Hdr.SetupSecs + 1) * 512;
    KernelLength = (UINT32) (KernelFfsSize - StartOffset);
    Temp = (UINT8 *) LoadedImage->ImageBase;

    DEBUG((DEBUG_INFO, "Kernel starts at offset 0x%X with length 0x%X\n", StartOffset, KernelLength));

    gBS->CopyMem(KernelBuffer, (Temp + StartOffset), KernelLength);
    DEBUG((DEBUG_INFO, "First 0x%X bytes of new kernel buffer contents:\n", MIN(0x100, KernelLength)));

    DumpHex(2, 0, MIN(0x100, KernelLength), KernelBuffer);

    // Prepare the command line
    Status = LoadLinuxSetCommandLine(HandoverParams, (UINT8 *) &CmdLine);
    if (EFI_ERROR (Status)) {
        DEBUG((EFI_D_INFO, "Unable to set linux command line; %r.\n", Status));
        goto FatalError;
    }

    HandoverParams->Hdr.Code32Start = (UINT32)(UINTN) KernelBuffer;
    HandoverParams->Hdr.LoaderId = 0x21;

    DEBUG((DEBUG_INFO, "Kernel loaded.\n"));

    gBS->UnloadImage(KernelHandle);
    gBS->FreePool(KernelFfsBuffer);
    KernelFfsBuffer = NULL;

    DEBUG((DEBUG_ERROR, "Launching the kernel\n"));


    //
    // Signal the EFI_EVENT_GROUP_READY_TO_BOOT event.
    //
    EfiSignalEventReadyToBoot();


    // Launch the kernel
    Status = LoadLinux(KernelBuffer, HandoverParams);

    ///
    /// LoadLinux should never return if the kernel boots. Anything past here is an error scenario
    ///
    DEBUG((DEBUG_ERROR, "ERROR: LoadLinux has returned with status; %r.\n", Status));

FatalError:
    // Free everything
    if (KernelBuffer != NULL) gBS->FreePages((EFI_PHYSICAL_ADDRESS) KernelBuffer, EFI_SIZE_TO_PAGES(HandoverParams->Hdr.InitSize));
    if (HandoverParams != NULL) gBS->FreePages((EFI_PHYSICAL_ADDRESS) HandoverParams, EFI_SIZE_TO_PAGES(KERNEL_SETUP_SIZE));
    if (KernelHandle != NULL) gBS->UnloadImage(KernelHandle);
    if (KernelFfsBuffer != NULL) gBS->FreePool(KernelFfsBuffer);

    return EFI_NOT_FOUND;
}

/**
 *  This is the main function for this feature. This will handle finding and launching
 *  the Linux kernel.
 *
 *  @note   In general, this function will never return to BDS. The LINUXBOOT_ALLOW_RETURN_TO_BDS
 *          token will allow you to return to BDS if the kernel fails to launch for some reason.
 *
 *  @param  None
 *
 *  @retval None
 */

EFI_STATUS
LinuxBootStart (
  VOID
  )
{
    EFI_STATUS  Status = EFI_SUCCESS;

    // Finish BDS and then try to launch the kernel
    //Status = CompleteBdsTasks();

    if (!EFI_ERROR(Status)) {
        LoadAndLaunchKernel();
    }

    DEBUG((DEBUG_ERROR, "-----------------------------------\n"));
    DEBUG((DEBUG_ERROR, "  ERROR: Kernel failed to launch.\n"));
    DEBUG((DEBUG_ERROR, "-----------------------------------\n"));
    return Status;
}

//---------------------------------------------------------------------------
