/** @file
*
*  Copyright (c) 2011 - 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 <Library/NetLib.h>
#include "BdsInternal.h"

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

  // The command line must be at least one character long
  ASSERT (MaxCmdLine > 0);

  // Ensure the last character of the buffer is the NULL character
  CmdLine[MaxCmdLine - 1] = '\0';

  Print (CmdLine);

  // To prevent a buffer overflow, we only allow to enter (MaxCmdLine-1) characters
  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"\r\n");

      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 if (CmdLineIndex < (MaxCmdLine-1)) {
      CmdLine[CmdLineIndex++] = Key.UnicodeChar;
      Print (L"%c", Key.UnicodeChar);
    }
  }

  return EFI_SUCCESS;
}

EFI_STATUS
GetHIInputStr (
  IN OUT CHAR16  *CmdLine,
  IN     UINTN   MaxCmdLine
  )
{
  EFI_STATUS  Status;

  // For a new input just passed an empty string
  CmdLine[0] = L'\0';

  Status = EditHIInputStr (CmdLine, MaxCmdLine);

  return Status;
}

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);
  if (!EFI_ERROR(Status)) {
    UnicodeStrToAsciiStr (Str, CmdLine);
  }
  FreePool (Str);

  return Status;
}

EFI_STATUS
GetHIInputAscii (
  IN OUT CHAR8   *CmdLine,
  IN     UINTN   MaxCmdLine
  )
{
  // For a new input just passed an empty string
  CmdLine[0] = '\0';

  return EditHIInputAscii (CmdLine,MaxCmdLine);
}

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;
}

/**
  Get an IPv4 address

  The function asks the user for an IPv4 address. If the input
  string defines a valid IPv4 address, the four bytes of the
  corresponding IPv4 address are extracted from the string and returned by
  the function. As long as the user does not define a valid IP
  address, he is asked for one. He can always escape by
  pressing ESC.

  @param[out]  EFI_IP_ADDRESS  OutIpAddr  Returned IPv4 address. Valid if
                                          and only if the returned value
                                          is equal to EFI_SUCCESS

  @retval  EFI_SUCCESS            Input completed
  @retval  EFI_ABORTED            Editing aborted by the user
  @retval  EFI_OUT_OF_RESOURCES   Fail to perform the operation due to
                                  lack of resource
**/
EFI_STATUS
GetHIInputIP (
  OUT  EFI_IP_ADDRESS  *OutIpAddr
  )
{
  EFI_STATUS  Status;
  CHAR16      CmdLine[48];

  while (TRUE) {
    CmdLine[0] = '\0';
    Status = EditHIInputStr (CmdLine, 48);
    if (EFI_ERROR (Status)) {
      return EFI_ABORTED;
    }

    Status = NetLibStrToIp4 (CmdLine, &OutIpAddr->v4);
    if (Status == EFI_INVALID_PARAMETER) {
      Print (L"Invalid address\n");
    } else {
      return Status;
    }
  }
}

/**
  Edit an IPv4 address

  The function displays as a string following the "%d.%d.%d.%d" format the
  IPv4 address that is passed in and asks the user to modify it. If the
  resulting string defines a valid IPv4 address, the four bytes of the
  corresponding IPv4 address are extracted from the string and returned by
  the function. As long as the user does not define a valid IP
  address, he is asked for one. He can always escape by
  pressing ESC.

  @param[in ]  EFI_IP_ADDRESS  InIpAddr   Input IPv4 address
  @param[out]  EFI_IP_ADDRESS  OutIpAddr  Returned IPv4 address. Valid if
                                          and only if the returned value
                                          is equal to EFI_SUCCESS

  @retval  EFI_SUCCESS            Update completed
  @retval  EFI_ABORTED            Editing aborted by the user
  @retval  EFI_INVALID_PARAMETER  The string returned by the user is
                                  mal-formated
  @retval  EFI_OUT_OF_RESOURCES   Fail to perform the operation due to
                                  lack of resource
**/
EFI_STATUS
EditHIInputIP (
  IN   EFI_IP_ADDRESS  *InIpAddr,
  OUT  EFI_IP_ADDRESS  *OutIpAddr
  )
{
  EFI_STATUS  Status;
  CHAR16      CmdLine[48];

  while (TRUE) {
    UnicodeSPrint (
      CmdLine, 48, L"%d.%d.%d.%d",
      InIpAddr->v4.Addr[0], InIpAddr->v4.Addr[1],
      InIpAddr->v4.Addr[2], InIpAddr->v4.Addr[3]
      );

    Status = EditHIInputStr (CmdLine, 48);
    if (EFI_ERROR (Status)) {
      return EFI_ABORTED;
    }
    Status = NetLibStrToIp4 (CmdLine, &OutIpAddr->v4);
    if (Status == EFI_INVALID_PARAMETER) {
      Print (L"Invalid address\n");
    } else {
      return Status;
    }
  }
}

EFI_STATUS
GetHIInputBoolean (
  OUT BOOLEAN *Value
  )
{
  CHAR16      CmdBoolean[2];
  EFI_STATUS  Status;

  while(1) {
    Print (L"[y/n] ");
    Status = GetHIInputStr (CmdBoolean, 2);
    if (EFI_ERROR(Status)) {
      return Status;
    } else if ((CmdBoolean[0] == L'y') || (CmdBoolean[0] == L'Y')) {
      if (Value) *Value = TRUE;
      return EFI_SUCCESS;
    } else if ((CmdBoolean[0] == L'n') || (CmdBoolean[0] == L'N')) {
      if (Value) *Value = FALSE;
      return EFI_SUCCESS;
    }
  }
}

BOOLEAN
HasFilePathEfiExtension (
  IN CHAR16* FilePath
  )
{
  return (StrCmp (FilePath + (StrSize (FilePath) / sizeof (CHAR16)) - 5, L".EFI") == 0) ||
         (StrCmp (FilePath + (StrSize (FilePath) / sizeof (CHAR16)) - 5, L".efi") == 0);
}

// Return the last non end-type Device Path Node from a Device Path
EFI_DEVICE_PATH*
GetLastDevicePathNode (
  IN EFI_DEVICE_PATH*  DevicePath
  )
{
  EFI_DEVICE_PATH*     PrevDevicePathNode;

  PrevDevicePathNode = DevicePath;
  while (!IsDevicePathEndType (DevicePath)) {
    PrevDevicePathNode = DevicePath;
    DevicePath = NextDevicePathNode (DevicePath);
  }

  return PrevDevicePathNode;
}

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;
}

EFI_STATUS
BdsStartBootOption (
  IN CHAR16* BootOption
  )
{
  EFI_STATUS          Status;
  BDS_LOAD_OPTION     *BdsLoadOption;

  Status = BootOptionFromLoadOptionVariable (BootOption, &BdsLoadOption);
  if (!EFI_ERROR(Status)) {
    Status = BootOptionStart (BdsLoadOption);
    FreePool (BdsLoadOption);

    if (!EFI_ERROR(Status)) {
      Status = EFI_SUCCESS;
    } else {
      Status = EFI_NOT_STARTED;
    }
  } else {
    Status = EFI_NOT_FOUND;
  }
  return Status;
}

UINTN
GetUnalignedDevicePathSize (
  IN EFI_DEVICE_PATH* DevicePath
  )
{
  UINTN Size;
  EFI_DEVICE_PATH* AlignedDevicePath;

  if ((UINTN)DevicePath & 0x1) {
    AlignedDevicePath = DuplicateDevicePath (DevicePath);
    Size = GetDevicePathSize (AlignedDevicePath);
    FreePool (AlignedDevicePath);
  } else {
    Size = GetDevicePathSize (DevicePath);
  }
  return Size;
}

EFI_DEVICE_PATH*
GetAlignedDevicePath (
  IN EFI_DEVICE_PATH* DevicePath
  )
{
  if ((UINTN)DevicePath & 0x1) {
    return DuplicateDevicePath (DevicePath);
  } else {
    return DevicePath;
  }
}

BOOLEAN
IsUnicodeString (
  IN VOID* String
  )
{
  // We do not support NULL pointer
  ASSERT (String != NULL);

  if (*(CHAR16*)String < 0x100) {
    //Note: We could get issue if the string is an empty Ascii string...
    return TRUE;
  } else {
    return FALSE;
  }
}

/*
 * Try to detect if the given string is an ASCII or Unicode string
 *
 * There are actually few limitation to this function but it is mainly to give
 * a user friendly output.
 *
 * Some limitations:
 *   - it only supports unicode string that use ASCII character (< 0x100)
 *   - single character ASCII strings are interpreted as Unicode string
 *   - string cannot be longer than BOOT_DEVICE_OPTION_MAX characters and
 *     thus (BOOT_DEVICE_OPTION_MAX*2) bytes for an Unicode string and
 *     BOOT_DEVICE_OPTION_MAX bytes for an ASCII string.
 *
 * @param String    Buffer that might contain a Unicode or Ascii string
 * @param IsUnicode If not NULL this boolean value returns if the string is an
 *                  ASCII or Unicode string.
 */
BOOLEAN
IsPrintableString (
  IN  VOID*    String,
  OUT BOOLEAN *IsUnicode
  )
{
  BOOLEAN UnicodeDetected;
  BOOLEAN IsPrintable;
  UINTN Index;
  CHAR16 Character;

  // We do not support NULL pointer
  ASSERT (String != NULL);

  // Test empty string
  if (*(CHAR16*)String == L'\0') {
    if (IsUnicode) {
      *IsUnicode = TRUE;
    }
    return TRUE;
  } else if (*(CHAR16*)String == '\0') {
    if (IsUnicode) {
      *IsUnicode = FALSE;
    }
    return TRUE;
  }

  // Limitation: if the string is an ASCII single character string. This comparison
  // will assume it is a Unicode string.
  if (*(CHAR16*)String < 0x100) {
    UnicodeDetected = TRUE;
  } else {
    UnicodeDetected = FALSE;
  }

  IsPrintable = FALSE;
  for (Index = 0; Index < BOOT_DEVICE_OPTION_MAX; Index++) {
    if (UnicodeDetected) {
      Character = ((CHAR16*)String)[Index];
    } else {
      Character = ((CHAR8*)String)[Index];
    }

    if (Character == '\0') {
      // End of the string
      IsPrintable = TRUE;
      break;
    } else if ((Character < 0x20) || (Character > 0x7f)) {
      // We only support the range of printable ASCII character
      IsPrintable = FALSE;
      break;
    }
  }

  if (IsPrintable && IsUnicode) {
    *IsUnicode = UnicodeDetected;
  }

  return IsPrintable;
}
