/** @file
*
*  Copyright (c) 2011-2013, 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 "LinuxInternal.h"

#define DEFAULT_BOOT_ENTRY_DESCRIPTION  L"Linux"
#define MAX_STR_INPUT                   300
#define MAX_ASCII_INPUT                 300

typedef enum {
  LINUX_LOADER_NEW = 1,
  LINUX_LOADER_UPDATE
} LINUX_LOADER_ACTION;

STATIC
EFI_STATUS
EditHIInputStr (
  IN OUT CHAR16  *CmdLine,
  IN     UINTN   MaxCmdLine
  )
{
  UINTN           CmdLineIndex;
  UINTN           WaitIndex;
  CHAR8           Char;
  EFI_INPUT_KEY   Key;
  EFI_STATUS      Status;

  Print (CmdLine);

  for (CmdLineIndex = StrLen (CmdLine); CmdLineIndex < MaxCmdLine; ) {
    Status = gBS->WaitForEvent (1, &gST->ConIn->WaitForKey, &WaitIndex);
    ASSERT_EFI_ERROR (Status);

    Status = gST->ConIn->ReadKeyStroke (gST->ConIn, &Key);
    ASSERT_EFI_ERROR (Status);

    // Unicode character is valid when Scancode is NUll
    if (Key.ScanCode == SCAN_NULL) {
      // Scan code is NUll, hence read Unicode character
      Char = (CHAR8)Key.UnicodeChar;
    } else {
      Char = CHAR_NULL;
    }

    if ((Char == CHAR_LINEFEED) || (Char == CHAR_CARRIAGE_RETURN) || (Char == 0x7f)) {
      CmdLine[CmdLineIndex] = '\0';
      Print (L"\n\r");

      return EFI_SUCCESS;
    } else if ((Key.UnicodeChar == L'\b') || (Key.ScanCode == SCAN_LEFT) || (Key.ScanCode == SCAN_DELETE)){
      if (CmdLineIndex != 0) {
        CmdLineIndex--;
        Print (L"\b \b");
      }
    } else if ((Key.ScanCode == SCAN_ESC) || (Char == 0x1B) || (Char == 0x0)) {
      return EFI_INVALID_PARAMETER;
    } else {
      CmdLine[CmdLineIndex++] = Key.UnicodeChar;
      Print (L"%c", Key.UnicodeChar);
    }
  }

  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
EditHIInputAscii (
  IN OUT CHAR8   *CmdLine,
  IN     UINTN   MaxCmdLine
  )
{
  CHAR16*     Str;
  EFI_STATUS  Status;

  Str = (CHAR16*)AllocatePool (MaxCmdLine * sizeof(CHAR16));
  AsciiStrToUnicodeStr (CmdLine, Str);

  Status = EditHIInputStr (Str, MaxCmdLine);

  UnicodeStrToAsciiStr (Str, CmdLine);
  FreePool (Str);

  return Status;
}

STATIC
EFI_STATUS
GetHIInputInteger (
  OUT UINTN   *Integer
  )
{
  CHAR16      CmdLine[255];
  EFI_STATUS  Status;

  CmdLine[0] = '\0';
  Status = EditHIInputStr (CmdLine, 255);
  if (!EFI_ERROR(Status)) {
    *Integer = StrDecimalToUintn (CmdLine);
  }

  return Status;
}

#if 0
EFI_STATUS
GenerateDeviceDescriptionName (
  IN  EFI_HANDLE  Handle,
  IN OUT CHAR16*  Description
  )
{
  EFI_STATUS                        Status;
  EFI_COMPONENT_NAME_PROTOCOL*      ComponentName2Protocol;
  EFI_DEVICE_PATH_TO_TEXT_PROTOCOL* DevicePathToTextProtocol;
  EFI_DEVICE_PATH_PROTOCOL*         DevicePathProtocol;
  CHAR16*                           DriverName;
  CHAR16*                           DevicePathTxt;
  EFI_DEVICE_PATH*                  DevicePathNode;

  ComponentName2Protocol = NULL;
  Status = gBS->HandleProtocol (Handle, &gEfiComponentName2ProtocolGuid, (VOID **)&ComponentName2Protocol);
  if (!EFI_ERROR(Status)) {
    //TODO: Fixme. we must find the best langague
    Status = ComponentName2Protocol->GetDriverName (ComponentName2Protocol,"en",&DriverName);
    if (!EFI_ERROR(Status)) {
      StrnCpy (Description,DriverName,BOOT_DEVICE_DESCRIPTION_MAX);
    }
  }

  if (EFI_ERROR(Status)) {
    // Use the lastest non null entry of the Device path as a description
    Status = gBS->HandleProtocol (Handle, &gEfiDevicePathProtocolGuid, (VOID **)&DevicePathProtocol);
    if (EFI_ERROR(Status)) {
      return Status;
    }

    // Convert the last non end-type Device Path Node in text for the description
    DevicePathNode = GetLastDevicePathNode (DevicePathProtocol);
    Status = gBS->LocateProtocol (&gEfiDevicePathToTextProtocolGuid, NULL, (VOID **)&DevicePathToTextProtocol);
    ASSERT_EFI_ERROR(Status);
    DevicePathTxt = DevicePathToTextProtocol->ConvertDevicePathToText(DevicePathNode,TRUE,TRUE);
    StrnCpy (Description, DevicePathTxt, BOOT_DEVICE_DESCRIPTION_MAX);
    FreePool (DevicePathTxt);
  }

  return EFI_SUCCESS;
}
#endif

EFI_STATUS
LinuxLoaderConfig (
  IN EFI_LOADED_IMAGE_PROTOCOL   *LoadedImage
  )
{
  EFI_STATUS                   Status;
  LINUX_LOADER_ACTION          Choice;
  UINTN                        BootOrderSize;
  UINT16*                      BootOrder;
  UINTN                        BootOrderCount;
  UINTN                        Index;
  CHAR16                       Description[MAX_ASCII_INPUT];
  CHAR8                        CmdLine[MAX_ASCII_INPUT];
  CHAR16                       Initrd[MAX_STR_INPUT];
  UINT16                       InitrdPathListLength;
  UINT16                       CmdLineLength;
  BDS_LOAD_OPTION*             BdsLoadOption;
  BDS_LOAD_OPTION**            SupportedBdsLoadOptions;
  UINTN                        SupportedBdsLoadOptionCount;
  LINUX_LOADER_OPTIONAL_DATA*  LinuxOptionalData;
  EFI_DEVICE_PATH*             DevicePathRoot;

  Choice = (LINUX_LOADER_ACTION)0;
  SupportedBdsLoadOptions = NULL;
  SupportedBdsLoadOptionCount = 0;

  do {
    Print (L"[%d] Create new Linux Boot Entry\n",LINUX_LOADER_NEW);
    Print (L"[%d] Update Linux Boot Entry\n",LINUX_LOADER_UPDATE);

    Print (L"Option: ");
    Status = GetHIInputInteger ((UINTN*)&Choice);
    if (Status == EFI_INVALID_PARAMETER) {
      Print (L"\n");
      return Status;
    } else if ((Choice != LINUX_LOADER_NEW) && (Choice != LINUX_LOADER_UPDATE)) {
      Print (L"Error: the option should be either '%d' or '%d'\n",LINUX_LOADER_NEW,LINUX_LOADER_UPDATE);
      Status = EFI_INVALID_PARAMETER;
    }
  } while (EFI_ERROR(Status));

  if (Choice == LINUX_LOADER_UPDATE) {
    // If no compatible entry then we just create a new entry
    Choice = LINUX_LOADER_NEW;

    // Scan the OptionalData of every entry for the correct signature
    Status = GetGlobalEnvironmentVariable (L"BootOrder", NULL, &BootOrderSize, (VOID**)&BootOrder);
    if (!EFI_ERROR(Status)) {
      BootOrderCount = BootOrderSize / sizeof(UINT16);

      // Allocate an array to handle maximum number of supported Boot Entry
      SupportedBdsLoadOptions = (BDS_LOAD_OPTION**)AllocatePool(sizeof(BDS_LOAD_OPTION*) * BootOrderCount);

      SupportedBdsLoadOptionCount = 0;

      // Check if the signature is present in the list of the current Boot entries
      for (Index = 0; Index < BootOrderCount; Index++) {
        Status = BootOptionFromLoadOptionIndex (BootOrder[Index], &BdsLoadOption);
        if (!EFI_ERROR(Status)) {
          if ((BdsLoadOption->OptionalDataSize >= sizeof(UINT32)) &&
              (*(UINT32*)BdsLoadOption->OptionalData == LINUX_LOADER_SIGNATURE)) {
            SupportedBdsLoadOptions[SupportedBdsLoadOptionCount++] = BdsLoadOption;
            Choice = LINUX_LOADER_UPDATE;
          }
        }
      }
    }
    FreePool (BootOrder);
  }

  if (Choice == LINUX_LOADER_NEW) {
    Description[0] = '\0';
    CmdLine[0]     = '\0';
    Initrd[0]      = '\0';

    BdsLoadOption = (BDS_LOAD_OPTION*)AllocateZeroPool (sizeof(BDS_LOAD_OPTION));

    DEBUG_CODE_BEGIN();
      CHAR16*                           DevicePathTxt;
      EFI_DEVICE_PATH_TO_TEXT_PROTOCOL* DevicePathToTextProtocol;

      Status = gBS->LocateProtocol (&gEfiDevicePathToTextProtocolGuid, NULL, (VOID **)&DevicePathToTextProtocol);
      ASSERT_EFI_ERROR(Status);
      DevicePathTxt = DevicePathToTextProtocol->ConvertDevicePathToText (LoadedImage->FilePath, TRUE, TRUE);

      Print(L"EFI OS Loader: %s\n",DevicePathTxt);

      FreePool(DevicePathTxt);
    DEBUG_CODE_END();

    //
    // Fill the known fields of BdsLoadOption
    //

    BdsLoadOption->Attributes = LOAD_OPTION_ACTIVE | LOAD_OPTION_CATEGORY_BOOT;

    // Get the full Device Path for this file
    Status = gBS->HandleProtocol (LoadedImage->DeviceHandle, &gEfiDevicePathProtocolGuid, (VOID **)&DevicePathRoot);
    ASSERT_EFI_ERROR(Status);

    BdsLoadOption->FilePathList = AppendDevicePath (DevicePathRoot, LoadedImage->FilePath);
    BdsLoadOption->FilePathListLength = GetDevicePathSize (BdsLoadOption->FilePathList);
  } else {
    if (SupportedBdsLoadOptionCount > 1) {
      for (Index = 0; Index < SupportedBdsLoadOptionCount; Index++) {
        Print (L"[%d] %s\n",Index + 1,SupportedBdsLoadOptions[Index]->Description);
      }

      do {
        Print (L"Update Boot Entry: ");
        Status = GetHIInputInteger ((UINTN*)&Choice);
        if (Status == EFI_INVALID_PARAMETER) {
          Print (L"\n");
          return Status;
        } else if ((Choice < 1) && (Choice > SupportedBdsLoadOptionCount)) {
          Print (L"Choose entry from 1 to %d\n",SupportedBdsLoadOptionCount);
          Status = EFI_INVALID_PARAMETER;
        }
      } while (EFI_ERROR(Status));
      BdsLoadOption = SupportedBdsLoadOptions[Choice-1];
    }
    StrnCpy (Description, BdsLoadOption->Description, MAX_STR_INPUT);

    LinuxOptionalData = (LINUX_LOADER_OPTIONAL_DATA*)BdsLoadOption->OptionalData;
    if (LinuxOptionalData->CmdLineLength > 0) {
      CopyMem (CmdLine, (CHAR8*)LinuxOptionalData + sizeof(LINUX_LOADER_OPTIONAL_DATA), LinuxOptionalData->CmdLineLength);
    } else {
      CmdLine[0] = '\0';
    }

    if (LinuxOptionalData->InitrdPathListLength > 0) {
      CopyMem (Initrd, (CHAR8*)LinuxOptionalData + sizeof(LINUX_LOADER_OPTIONAL_DATA) + LinuxOptionalData->CmdLineLength, LinuxOptionalData->InitrdPathListLength);
    } else {
      Initrd[0] = L'\0';
    }
    DEBUG((EFI_D_ERROR,"L\n"));
  }

  // Description
  Print (L"Description: ");
  Status = EditHIInputStr (Description, MAX_STR_INPUT);
  if (EFI_ERROR(Status)) {
    return Status;
  }
  if (StrLen (Description) == 0) {
    StrnCpy (Description, DEFAULT_BOOT_ENTRY_DESCRIPTION, MAX_STR_INPUT);
  }
  BdsLoadOption->Description = Description;

  // CmdLine
  Print (L"Command Line: ");
  Status = EditHIInputAscii (CmdLine, MAX_ASCII_INPUT);
  if (EFI_ERROR(Status)) {
    return Status;
  }

  // Initrd
  Print (L"Initrd name: ");
  Status = EditHIInputStr (Initrd, MAX_STR_INPUT);
  if (EFI_ERROR(Status)) {
    return Status;
  }

  CmdLineLength = AsciiStrLen (CmdLine);
  if (CmdLineLength > 0) {
    CmdLineLength += sizeof(CHAR8);
  }

  InitrdPathListLength = StrLen (Initrd) * sizeof(CHAR16);
  if (InitrdPathListLength > 0) {
    InitrdPathListLength += sizeof(CHAR16);
  }

  BdsLoadOption->OptionalDataSize = sizeof(LINUX_LOADER_OPTIONAL_DATA) + CmdLineLength + InitrdPathListLength;

  LinuxOptionalData = (LINUX_LOADER_OPTIONAL_DATA*)AllocatePool (BdsLoadOption->OptionalDataSize);
  BdsLoadOption->OptionalData = LinuxOptionalData;

  LinuxOptionalData->Signature = LINUX_LOADER_SIGNATURE;
  LinuxOptionalData->CmdLineLength = CmdLineLength;
  LinuxOptionalData->InitrdPathListLength = InitrdPathListLength;

  if (CmdLineLength > 0) {
    CopyMem (LinuxOptionalData + 1, CmdLine, CmdLineLength);
  }
  if (InitrdPathListLength > 0) {
    CopyMem ((UINT8*)(LinuxOptionalData + 1) + CmdLineLength, Initrd, InitrdPathListLength);
  }

  // Create or Update the boot entry
  Status = BootOptionToLoadOptionVariable (BdsLoadOption);

  return Status;
}
