/** @file

  Copyright (c) 2020 - 2021, Ampere Computing LLC. All rights reserved.<BR>

  SPDX-License-Identifier: BSD-2-Clause-Patent

**/

#include <Uefi.h>

#include <AcpiConfigNVDataStruct.h>
#include <Guid/AcpiConfigHii.h>
#include <Guid/MdeModuleHii.h>
#include <Guid/PlatformInfoHob.h>
#include <Library/AcpiLib.h>
#include <Library/BaseLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/DebugLib.h>
#include <Library/DevicePathLib.h>
#include <Library/HiiLib.h>
#include <Library/HobLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/SystemFirmwareInterfaceLib.h>
#include <Library/PrintLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiDriverEntryPoint.h>
#include <Library/UefiLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>
#include <Protocol/AcpiSystemDescriptionTable.h>
#include <Protocol/HiiConfigAccess.h>
#include <Protocol/HiiConfigRouting.h>

#include "AcpiConfigDxe.h"

#define ACPI_VARSTORE_ATTRIBUTES EFI_VARIABLE_BOOTSERVICE_ACCESS | \
                                 EFI_VARIABLE_RUNTIME_ACCESS     | \
                                 EFI_VARIABLE_NON_VOLATILE

CHAR16 AcpiVarstoreDataName[] = L"AcpiConfigNVData";

EFI_HANDLE               mDriverHandle = NULL;
ACPI_CONFIG_PRIVATE_DATA *mPrivateData = NULL;

HII_VENDOR_DEVICE_PATH mAcpiConfigHiiVendorDevicePath = {
  {
    {
      HARDWARE_DEVICE_PATH,
      HW_VENDOR_DP,
      {
        (UINT8)(sizeof (VENDOR_DEVICE_PATH)),
        (UINT8)((sizeof (VENDOR_DEVICE_PATH)) >> 8)
      }
    },
    ACPI_CONFIGURATION_FORMSET_GUID
  },
  {
    END_DEVICE_PATH_TYPE,
    END_ENTIRE_DEVICE_PATH_SUBTYPE,
    {
      (UINT8)(END_DEVICE_PATH_LENGTH),
      (UINT8)((END_DEVICE_PATH_LENGTH) >> 8)
    }
  }
};

/**
  This function allows a caller to extract the current configuration for one
  or more named elements from the target driver.

  @param  This                   Points to the EFI_HII_CONFIG_ACCESS_PROTOCOL.
  @param  Request                A null-terminated Unicode string in
                                 <ConfigRequest> format.
  @param  Progress               On return, points to a character in the Request
                                 string. Points to the string's null terminator if
                                 request was successful. Points to the most recent
                                 '&' before the first failing name/value pair (or
                                 the beginning of the string if the failure is in
                                 the first name/value pair) if the request was not
                                 successful.
  @param  Results                A null-terminated Unicode string in
                                 <ConfigAltResp> format which has all values filled
                                 in for the names in the Request string. String to
                                 be allocated by the called function.

  @retval EFI_SUCCESS            The Results is filled with the requested values.
  @retval EFI_OUT_OF_RESOURCES   Not enough memory to store the results.
  @retval EFI_INVALID_PARAMETER  Request is illegal syntax, or unknown name.
  @retval EFI_NOT_FOUND          Routing data doesn't match any storage in this
                                 driver.

**/
EFI_STATUS
EFIAPI
ExtractConfig (
  IN  CONST EFI_HII_CONFIG_ACCESS_PROTOCOL *This,
  IN  CONST EFI_STRING                     Request,
  OUT EFI_STRING                           *Progress,
  OUT EFI_STRING                           *Results
  )
{
  EFI_STATUS                      Status;
  UINTN                           BufferSize;
  ACPI_CONFIG_PRIVATE_DATA        *PrivateData;
  EFI_HII_CONFIG_ROUTING_PROTOCOL *HiiConfigRouting;
  EFI_STRING                      ConfigRequest;
  EFI_STRING                      ConfigRequestHdr;
  UINTN                           Size;
  BOOLEAN                         AllocatedRequest;

  if (Progress == NULL || Results == NULL) {
    return EFI_INVALID_PARAMETER;
  }
  //
  // Initialize the local variables.
  //
  ConfigRequestHdr  = NULL;
  ConfigRequest     = NULL;
  Size              = 0;
  *Progress         = Request;
  AllocatedRequest  = FALSE;

  if ((Request != NULL) && !HiiIsConfigHdrMatch (Request, &gAcpiConfigFormSetGuid, AcpiVarstoreDataName)) {
    return EFI_NOT_FOUND;
  }

  PrivateData = ACPI_CONFIG_PRIVATE_FROM_THIS (This);
  HiiConfigRouting = PrivateData->HiiConfigRouting;

  //
  // Get Buffer Storage data from EFI variable.
  // Try to get the current setting from variable.
  //
  BufferSize = sizeof (ACPI_CONFIG_VARSTORE_DATA);
  Status = gRT->GetVariable (
                  AcpiVarstoreDataName,
                  &gAcpiConfigFormSetGuid,
                  NULL,
                  &BufferSize,
                  &PrivateData->Configuration
                  );
  if (EFI_ERROR (Status)) {
    return EFI_NOT_FOUND;
  }

  //
  // Convert buffer data to <ConfigResp> by helper function BlockToConfig()
  //
  BufferSize = sizeof (ACPI_CONFIG_VARSTORE_DATA);
  ConfigRequest = Request;
  if ((Request == NULL) || (StrStr (Request, L"OFFSET") == NULL)) {
    //
    // Request has no request element, construct full request string.
    // Allocate and fill a buffer large enough to hold the <ConfigHdr> template
    // followed by "&OFFSET=0&WIDTH=WWWWWWWWWWWWWWWW" followed by a Null-terminator
    //
    ConfigRequestHdr = HiiConstructConfigHdr (
                         &gAcpiConfigFormSetGuid,
                         AcpiVarstoreDataName,
                         PrivateData->DriverHandle
                         );
    Size = (StrLen (ConfigRequestHdr) + 32 + 1) * sizeof (CHAR16);
    ConfigRequest = AllocateZeroPool (Size);
    ASSERT (ConfigRequest != NULL);
    if (ConfigRequest == NULL) {
      return EFI_OUT_OF_RESOURCES;
    }
    AllocatedRequest = TRUE;
    UnicodeSPrint (ConfigRequest, Size, L"%s&OFFSET=0&WIDTH=%016LX", ConfigRequestHdr, (UINT64)BufferSize);
    FreePool (ConfigRequestHdr);
  }

  //
  // Convert buffer data to <ConfigResp> by helper function BlockToConfig()
  //
  Status = HiiConfigRouting->BlockToConfig (
                               HiiConfigRouting,
                               ConfigRequest,
                               (UINT8 *)&PrivateData->Configuration,
                               BufferSize,
                               Results,
                               Progress
                               );

  //
  // Free the allocated config request string.
  //
  if (AllocatedRequest) {
    FreePool (ConfigRequest);
    ConfigRequest = NULL;
  }

  //
  // Set Progress string to the original request string.
  //
  if (Request == NULL) {
    *Progress = NULL;
  } else if (StrStr (Request, L"OFFSET") == NULL) {
    *Progress = Request + StrLen (Request);
  }

  return Status;
}

/**
  This function processes the results of changes in configuration.

  @param  This                   Points to the EFI_HII_CONFIG_ACCESS_PROTOCOL.
  @param  Configuration          A null-terminated Unicode string in <ConfigResp>
                                 format.
  @param  Progress               A pointer to a string filled in with the offset of
                                 the most recent '&' before the first failing
                                 name/value pair (or the beginning of the string if
                                 the failure is in the first name/value pair) or
                                 the terminating NULL if all was successful.

  @retval EFI_SUCCESS            The Results is processed successfully.
  @retval EFI_INVALID_PARAMETER  Configuration is NULL.
  @retval EFI_NOT_FOUND          Routing data doesn't match any storage in this
                                 driver.

**/
EFI_STATUS
EFIAPI
RouteConfig (
  IN  CONST EFI_HII_CONFIG_ACCESS_PROTOCOL *This,
  IN  CONST EFI_STRING                     Configuration,
  OUT EFI_STRING                           *Progress
  )
{
  EFI_STATUS                      Status;
  UINTN                           BufferSize;
  ACPI_CONFIG_PRIVATE_DATA        *PrivateData;
  EFI_HII_CONFIG_ROUTING_PROTOCOL *HiiConfigRouting;

  if (Configuration == NULL || Progress == NULL) {
    return EFI_INVALID_PARAMETER;
  }

  PrivateData = ACPI_CONFIG_PRIVATE_FROM_THIS (This);
  HiiConfigRouting = PrivateData->HiiConfigRouting;
  *Progress = Configuration;

  //
  // Check routing data in <ConfigHdr>.
  // Note: if only one Storage is used, then this checking could be skipped.
  //
  if (!HiiIsConfigHdrMatch (Configuration, &gAcpiConfigFormSetGuid, AcpiVarstoreDataName)) {
    return EFI_NOT_FOUND;
  }

  //
  // Get Buffer Storage data from EFI variable
  //
  BufferSize = sizeof (ACPI_CONFIG_VARSTORE_DATA);
  Status = gRT->GetVariable (
                  AcpiVarstoreDataName,
                  &gAcpiConfigFormSetGuid,
                  NULL,
                  &BufferSize,
                  &PrivateData->Configuration
                  );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  //
  // Convert <ConfigResp> to buffer data by helper function ConfigToBlock()
  //
  BufferSize = sizeof (ACPI_CONFIG_VARSTORE_DATA);
  Status = HiiConfigRouting->ConfigToBlock (
                               HiiConfigRouting,
                               Configuration,
                               (UINT8 *)&PrivateData->Configuration,
                               &BufferSize,
                               Progress
                               );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  //
  // Store Buffer Storage back to EFI variable
  //
  Status = gRT->SetVariable (
                  AcpiVarstoreDataName,
                  &gAcpiConfigFormSetGuid,
                  ACPI_VARSTORE_ATTRIBUTES,
                  sizeof (ACPI_CONFIG_VARSTORE_DATA),
                  &PrivateData->Configuration
                  );

  return Status;
}

/**
  This function processes the results of changes in configuration.

  @param  This                   Points to the EFI_HII_CONFIG_ACCESS_PROTOCOL.
  @param  Action                 Specifies the type of action taken by the browser.
  @param  QuestionId             A unique value which is sent to the original
                                 exporting driver so that it can identify the type
                                 of data to expect.
  @param  Type                   The type of value for the question.
  @param  Value                  A pointer to the data being sent to the original
                                 exporting driver.
  @param  ActionRequest          On return, points to the action requested by the
                                 callback function.

  @retval  EFI_SUCCESS           The callback successfully handled the action.
  @retval  EFI_INVALID_PARAMETER The setup browser call this function with invalid parameters.

**/
EFI_STATUS
EFIAPI
DriverCallback (
  IN  CONST EFI_HII_CONFIG_ACCESS_PROTOCOL *This,
  IN  EFI_BROWSER_ACTION                   Action,
  IN  EFI_QUESTION_ID                      QuestionId,
  IN  UINT8                                Type,
  IN  EFI_IFR_TYPE_VALUE                   *Value,
  OUT EFI_BROWSER_ACTION_REQUEST           *ActionRequest
  )
{
  if (Action != EFI_BROWSER_ACTION_CHANGING) {
    //
    // Do nothing for other UEFI Action. Only do call back when data is changed.
    //
    return EFI_UNSUPPORTED;
  }
  if (((Value == NULL)
       && (Action != EFI_BROWSER_ACTION_FORM_OPEN)
       && (Action != EFI_BROWSER_ACTION_FORM_CLOSE))
      || (ActionRequest == NULL))
  {
    return EFI_INVALID_PARAMETER;
  }

  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
AcpiNVDataUpdate (
  IN ACPI_CONFIG_PRIVATE_DATA *PrivateData
  )
{
  EFI_STATUS         Status;
  PLATFORM_INFO_HOB  *PlatformHob;
  UINT32             TurboSupport;

  ASSERT (PrivateData != NULL);

  PlatformHob = PrivateData->PlatformHob;
  TurboSupport = PlatformHob->TurboCapability[0] + PlatformHob->TurboCapability[1];

  if (TurboSupport == 0) {
    PrivateData->Configuration.AcpiTurboMode = 2; // Unsupported mode
    PrivateData->Configuration.AcpiTurboSupport = 0;
  } else {
    PrivateData->Configuration.AcpiTurboSupport = 1;
  }

  Status = gRT->SetVariable (
                  AcpiVarstoreDataName,
                  &gAcpiConfigFormSetGuid,
                  ACPI_VARSTORE_ATTRIBUTES,
                  sizeof (ACPI_CONFIG_VARSTORE_DATA),
                  &PrivateData->Configuration
                  );
  if (EFI_ERROR (Status)) {
    DEBUG ((DEBUG_ERROR, "%a %d gRT->SetVariable() failed \n", __FUNCTION__, __LINE__));
    return Status;
  }

  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
UpdateTurboModeConfig (
  IN ACPI_CONFIG_PRIVATE_DATA *PrivateData
  )
{
  EFI_STATUS         Status;
  PLATFORM_INFO_HOB  *PlatformHob;
  BOOLEAN            EnableTurbo;

  ASSERT (PrivateData != NULL);

  if (PrivateData->Configuration.AcpiTurboSupport != 0) {
    PlatformHob = PrivateData->PlatformHob;
    EnableTurbo = (PrivateData->Configuration.AcpiTurboMode != 0) ? TRUE : FALSE;

    if (PlatformHob->TurboCapability[0] != 0) {
      Status = MailboxMsgTurboConfig (0, EnableTurbo);
      if (EFI_ERROR (Status)) {
        return Status;
      }
    }

    if (PlatformHob->TurboCapability[1] != 0) {
      Status = MailboxMsgTurboConfig (1, EnableTurbo);
      if (EFI_ERROR (Status)) {
        return Status;
      }
    }
  } else {
    DEBUG ((DEBUG_INFO, "%a: Turbo mode is unsupported! \n", __FUNCTION__));
  }

  return EFI_SUCCESS;
}

STATIC
EFI_STATUS
UpdateCPPCConfig (
  IN ACPI_CONFIG_PRIVATE_DATA *PrivateData
  )
{
  EFI_STATUS            Status;
  CHAR8                 Buffer[64];

  ASSERT (PrivateData != NULL);

  AsciiSPrint (Buffer, sizeof (Buffer), "\\_SB.CPCE");
  Status = AcpiAmlObjectUpdateInteger (
             PrivateData->AcpiSdtProtocol,
             PrivateData->AcpiTableHandle,
             Buffer,
             PrivateData->Configuration.AcpiCppcEnable
             );
  ASSERT_EFI_ERROR (Status);

  return Status;
}

STATIC
EFI_STATUS
UpdateLPIConfig (
  IN ACPI_CONFIG_PRIVATE_DATA *PrivateData
  )
{
  EFI_STATUS            Status;
  CHAR8                 Buffer[64];

  ASSERT (PrivateData != NULL);

  AsciiSPrint (Buffer, sizeof (Buffer), "\\_SB.LPIE");
  Status = AcpiAmlObjectUpdateInteger (
             PrivateData->AcpiSdtProtocol,
             PrivateData->AcpiTableHandle,
             Buffer,
             PrivateData->Configuration.AcpiLpiEnable
             );
  ASSERT_EFI_ERROR (Status);

  return Status;
}

STATIC
VOID
UpdateAcpiOnReadyToBoot (
  IN EFI_EVENT Event,
  IN VOID      *Context
  )
{
  EFI_STATUS                    Status;
  EFI_ACPI_SDT_PROTOCOL         *AcpiSdtProtocol;
  EFI_ACPI_DESCRIPTION_HEADER   *Table;
  EFI_ACPI_HANDLE               TableHandle;
  UINTN                         TableKey;
  UINTN                         TableIndex;

  ASSERT (mPrivateData != NULL);

  //
  // Find the AcpiTable protocol
  //
  Status = gBS->LocateProtocol (
                  &gEfiAcpiSdtProtocolGuid,
                  NULL,
                  (VOID **)&AcpiSdtProtocol
                  );
  if (EFI_ERROR (Status)) {
    DEBUG ((DEBUG_ERROR, "Unable to locate ACPI table protocol\n"));
    return;
  }

  mPrivateData->AcpiSdtProtocol = AcpiSdtProtocol;

  TableIndex = 0;
  Status = AcpiLocateTableBySignature (
             AcpiSdtProtocol,
             EFI_ACPI_6_3_DIFFERENTIATED_SYSTEM_DESCRIPTION_TABLE_SIGNATURE,
             &TableIndex,
             &Table,
             &TableKey
             );
  ASSERT_EFI_ERROR (Status);
  if (EFI_ERROR (Status)) {
    return;
  }

  Status = AcpiSdtProtocol->OpenSdt (TableKey, &TableHandle);
  ASSERT_EFI_ERROR (Status);
  if (EFI_ERROR (Status)) {
    return;
  }

  mPrivateData->AcpiTableHandle = TableHandle;

  Status = UpdateCPPCConfig (mPrivateData);
  if (EFI_ERROR (Status)) {
    return;
  }

  Status = UpdateLPIConfig (mPrivateData);
  if (EFI_ERROR (Status)) {
    return;
  }

  Status = UpdateTurboModeConfig (mPrivateData);
  if (EFI_ERROR (Status)) {
    return;
  }

  //
  // Close DSDT Table
  //
  AcpiSdtProtocol->Close (TableHandle);
  AcpiUpdateChecksum ((UINT8 *)Table, Table->Length);
}

STATIC
EFI_STATUS
AcpiConfigUnload (
  VOID
  )
{
  ASSERT (mPrivateData != NULL);

  if (mDriverHandle != NULL) {
    gBS->UninstallMultipleProtocolInterfaces (
           mDriverHandle,
           &gEfiDevicePathProtocolGuid,
           &mAcpiConfigHiiVendorDevicePath,
           &gEfiHiiConfigAccessProtocolGuid,
           &mPrivateData->ConfigAccess,
           NULL
           );
    mDriverHandle = NULL;
  }

  if (mPrivateData->HiiHandle != NULL) {
    HiiRemovePackages (mPrivateData->HiiHandle);
  }

  FreePool (mPrivateData);
  mPrivateData = NULL;

  return EFI_SUCCESS;
}

EFI_STATUS
EFIAPI
AcpiConfigEntryPoint (
  IN EFI_HANDLE       ImageHandle,
  IN EFI_SYSTEM_TABLE *SystemTable
  )
{
  EFI_STATUS                      Status;
  EFI_HII_HANDLE                  HiiHandle;
  EFI_HII_CONFIG_ROUTING_PROTOCOL *HiiConfigRouting;
  UINTN                           BufferSize;
  ACPI_CONFIG_VARSTORE_DATA       *Configuration;
  BOOLEAN                         ActionFlag;
  EFI_STRING                      ConfigRequestHdr;
  EFI_EVENT                       ReadyToBootEvent;
  PLATFORM_INFO_HOB               *PlatformHob;
  VOID                            *Hob;

  //
  // Initialize the local variables.
  //
  ConfigRequestHdr = NULL;

  //
  // Initialize driver private data
  //
  mPrivateData = AllocateZeroPool (sizeof (ACPI_CONFIG_PRIVATE_DATA));
  if (mPrivateData == NULL) {
    return EFI_OUT_OF_RESOURCES;
  }

  mPrivateData->Signature = ACPI_CONFIG_PRIVATE_SIGNATURE;

  mPrivateData->ConfigAccess.ExtractConfig = ExtractConfig;
  mPrivateData->ConfigAccess.RouteConfig = RouteConfig;
  mPrivateData->ConfigAccess.Callback = DriverCallback;

  //
  // Get the Platform HOB
  //
  Hob = GetFirstGuidHob (&gPlatformInfoHobGuid);
  ASSERT (Hob != NULL);
  if (Hob == NULL) {
    AcpiConfigUnload ();
    return EFI_DEVICE_ERROR;
  }
  PlatformHob = (PLATFORM_INFO_HOB *)GET_GUID_HOB_DATA (Hob);

  mPrivateData->PlatformHob = PlatformHob;

  //
  // Locate ConfigRouting protocol
  //
  Status = gBS->LocateProtocol (&gEfiHiiConfigRoutingProtocolGuid, NULL, (VOID **)&HiiConfigRouting);
  if (EFI_ERROR (Status)) {
    return Status;
  }
  mPrivateData->HiiConfigRouting = HiiConfigRouting;

  Status = gBS->InstallMultipleProtocolInterfaces (
                  &mDriverHandle,
                  &gEfiDevicePathProtocolGuid,
                  &mAcpiConfigHiiVendorDevicePath,
                  &gEfiHiiConfigAccessProtocolGuid,
                  &mPrivateData->ConfigAccess,
                  NULL
                  );
  ASSERT_EFI_ERROR (Status);

  mPrivateData->DriverHandle = mDriverHandle;

  //
  // Publish our HII data
  //
  HiiHandle = HiiAddPackages (
                &gAcpiConfigFormSetGuid,
                mDriverHandle,
                AcpiConfigDxeStrings,
                AcpiConfigVfrBin,
                NULL
                );
  if (HiiHandle == NULL) {
    gBS->UninstallMultipleProtocolInterfaces (
           mDriverHandle,
           &gEfiDevicePathProtocolGuid,
           &mAcpiConfigHiiVendorDevicePath,
           &gEfiHiiConfigAccessProtocolGuid,
           &mPrivateData->ConfigAccess,
           NULL
           );
    return EFI_OUT_OF_RESOURCES;
  }

  mPrivateData->HiiHandle = HiiHandle;

  //
  // Initialize configuration data
  //
  Configuration = &mPrivateData->Configuration;
  ZeroMem (Configuration, sizeof (ACPI_CONFIG_VARSTORE_DATA));

  //
  // Try to read NV config EFI variable first
  //
  ConfigRequestHdr = HiiConstructConfigHdr (&gAcpiConfigFormSetGuid, AcpiVarstoreDataName, mDriverHandle);
  ASSERT (ConfigRequestHdr != NULL);

  BufferSize = sizeof (ACPI_CONFIG_VARSTORE_DATA);
  Status = gRT->GetVariable (AcpiVarstoreDataName, &gAcpiConfigFormSetGuid, NULL, &BufferSize, Configuration);
  if (EFI_ERROR (Status)) {
    //
    // Store zero data Buffer Storage to EFI variable
    //
    Status = gRT->SetVariable (
                    AcpiVarstoreDataName,
                    &gAcpiConfigFormSetGuid,
                    ACPI_VARSTORE_ATTRIBUTES,
                    sizeof (ACPI_CONFIG_VARSTORE_DATA),
                    Configuration
                    );
    if (EFI_ERROR (Status)) {
      AcpiConfigUnload ();
      return Status;
    }
    //
    // EFI variable for NV config doesn't exit, we should build this variable
    // based on default values stored in IFR
    //
    ActionFlag = HiiSetToDefaults (ConfigRequestHdr, EFI_HII_DEFAULT_CLASS_STANDARD);
    if (!ActionFlag) {
      AcpiConfigUnload ();
      return EFI_INVALID_PARAMETER;
    }
  } else {
    //
    // EFI variable does exist and Validate Current Setting
    //
    ActionFlag = HiiValidateSettings (ConfigRequestHdr);
    if (!ActionFlag) {
      AcpiConfigUnload ();
      return EFI_INVALID_PARAMETER;
    }
  }
  FreePool (ConfigRequestHdr);

  Status = gBS->CreateEventEx (
                  EVT_NOTIFY_SIGNAL,
                  TPL_NOTIFY,
                  UpdateAcpiOnReadyToBoot,
                  NULL,
                  &gEfiEventReadyToBootGuid,
                  &ReadyToBootEvent
                  );
  if (EFI_ERROR (Status)) {
    DEBUG ((DEBUG_ERROR, "Fail to create ready to boot event %r!\n", Status));
    return Status;
  }

  Status = AcpiNVDataUpdate (mPrivateData);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  return EFI_SUCCESS;
}
