/** @file
  SSDT Serial Port Fixup Library.

  Copyright (c) 2019 - 2024, Arm Limited. All rights reserved.<BR>

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

  @par Reference(s):
  - Arm Server Base Boot Requirements (SBBR), s4.2.1.8 "SPCR".
  - Microsoft Debug Port Table 2 (DBG2) Specification - December 10, 2015.
  - ACPI for Arm Components 1.0 - 2020
  - Arm Generic Interrupt Controller Architecture Specification,
    Issue H, January 2022.
    (https://developer.arm.com/documentation/ihi0069/)
**/

#include <IndustryStandard/DebugPort2Table.h>
#include <Library/AcpiLib.h>
#include <Library/BaseLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/DebugLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Protocol/AcpiTable.h>

// Module specific include files.
#include <AcpiTableGenerator.h>
#include <ConfigurationManagerObject.h>
#include <ConfigurationManagerHelper.h>
#include <Library/AcpiHelperLib.h>
#include <Library/AmlLib/AmlLib.h>
#include <Protocol/ConfigurationManagerProtocol.h>

#if defined (MDE_CPU_AARCH64)
  #include <Library/ArmGicLib.h>
#endif

/** C array containing the compiled AML template.
    This symbol is defined in the auto generated C file
    containing the AML bytecode array.
*/
extern CHAR8  ssdtserialporttemplate_aml_code[];

/** UART address range length.
*/
#define MIN_UART_ADDRESS_LENGTH  0x1000U

/** Validate the Serial Port Information.

  @param [in]  SerialPortInfoTable    Table of CM_ARCH_COMMON_SERIAL_PORT_INFO.
  @param [in]  SerialPortCount        Count of SerialPort in the table.

  @retval EFI_SUCCESS             Success.
  @retval EFI_INVALID_PARAMETER   Invalid parameter.
**/
EFI_STATUS
EFIAPI
ValidateSerialPortInfo (
  IN  CONST CM_ARCH_COMMON_SERIAL_PORT_INFO  *SerialPortInfoTable,
  IN        UINT32                           SerialPortCount
  )
{
  UINT32                                 Index;
  CONST CM_ARCH_COMMON_SERIAL_PORT_INFO  *SerialPortInfo;

  if ((SerialPortInfoTable == NULL)  ||
      (SerialPortCount == 0))
  {
    ASSERT (0);
    return EFI_INVALID_PARAMETER;
  }

  for (Index = 0; Index < SerialPortCount; Index++) {
    SerialPortInfo = &SerialPortInfoTable[Index];
    ASSERT (SerialPortInfo != NULL);

    if ((SerialPortInfo == NULL) ||
        (SerialPortInfo->BaseAddress == 0))
    {
      DEBUG ((
        DEBUG_ERROR,
        "ERROR: UART port base address is invalid. BaseAddress = 0x%llx\n",
        SerialPortInfo->BaseAddress
        ));
      return EFI_INVALID_PARAMETER;
    }

    if ((SerialPortInfo->PortSubtype !=
         EFI_ACPI_DBG2_PORT_SUBTYPE_SERIAL_ARM_PL011_UART) &&
        (SerialPortInfo->PortSubtype !=
         EFI_ACPI_DBG2_PORT_SUBTYPE_SERIAL_ARM_SBSA_GENERIC_UART_2X) &&
        (SerialPortInfo->PortSubtype !=
         EFI_ACPI_DBG2_PORT_SUBTYPE_SERIAL_ARM_SBSA_GENERIC_UART) &&
        (SerialPortInfo->PortSubtype !=
         EFI_ACPI_DBG2_PORT_SUBTYPE_SERIAL_DCC) &&
        (SerialPortInfo->PortSubtype !=
         EFI_ACPI_DBG2_PORT_SUBTYPE_SERIAL_FULL_16550) &&
        (SerialPortInfo->PortSubtype !=
         EFI_ACPI_DBG2_PORT_SUBTYPE_SERIAL_16550_WITH_GAS))
    {
      DEBUG ((
        DEBUG_ERROR,
        "ERROR: UART port subtype is invalid."
        " UART Base  = 0x%llx, PortSubtype = 0x%x\n",
        SerialPortInfo->BaseAddress,
        SerialPortInfo->PortSubtype
        ));
      return EFI_INVALID_PARAMETER;
    }

 #if defined (MDE_CPU_AARCH64)
    // If an interrupt is not wired to the serial port, the Configuration
    // Manager specifies the interrupt as 0.
    // Any other value must be within the SPI or extended SPI range.
    if ((SerialPortInfo->Interrupt != 0) &&
        !(((SerialPortInfo->Interrupt >= ARM_GIC_ARCH_SPI_MIN) &&
           (SerialPortInfo->Interrupt <= ARM_GIC_ARCH_SPI_MAX)) ||
          ((SerialPortInfo->Interrupt >= ARM_GIC_ARCH_EXT_SPI_MIN) &&
           (SerialPortInfo->Interrupt <= ARM_GIC_ARCH_EXT_SPI_MAX))))
    {
      DEBUG ((
        DEBUG_ERROR,
        "ERROR: Invalid UART port interrupt ID. Interrupt = %lu\n",
        SerialPortInfo->Interrupt
        ));
      return EFI_INVALID_PARAMETER;
    }

 #endif

    DEBUG ((DEBUG_INFO, "UART Configuration:\n"));
    DEBUG ((
      DEBUG_INFO,
      "  UART Base  = 0x%llx\n",
      SerialPortInfo->BaseAddress
      ));
    DEBUG ((
      DEBUG_INFO,
      "  Length     = 0x%llx\n",
      SerialPortInfo->BaseAddressLength
      ));
    DEBUG ((DEBUG_INFO, "  Clock      = %lu\n", SerialPortInfo->Clock));
    DEBUG ((DEBUG_INFO, "  BaudRate   = %llu\n", SerialPortInfo->BaudRate));
    DEBUG ((DEBUG_INFO, "  Interrupt  = %lu\n", SerialPortInfo->Interrupt));
  } // for

  return EFI_SUCCESS;
}

/** Fixup the Serial Port Ids (_UID, _HID, _CID).

  @param  [in]  RootNodeHandle  Pointer to the root of an AML tree.
  @param  [in]  Uid             UID for the Serial Port.
  @param  [in]  SerialPortInfo  Pointer to a Serial Port Information
                                structure.
                                Get the Serial Port Information from there.

  @retval  EFI_SUCCESS            The function completed successfully.
  @retval  EFI_INVALID_PARAMETER  Invalid parameter.
  @retval  EFI_NOT_FOUND          Could not find information.
  @retval  EFI_OUT_OF_RESOURCES   Out of resources.
**/
STATIC
EFI_STATUS
EFIAPI
FixupIds (
  IN        AML_ROOT_NODE_HANDLE             RootNodeHandle,
  IN  CONST UINT64                           Uid,
  IN  CONST CM_ARCH_COMMON_SERIAL_PORT_INFO  *SerialPortInfo
  )
{
  EFI_STATUS              Status;
  AML_OBJECT_NODE_HANDLE  NameOpIdNode;
  CONST CHAR8             *HidString;
  CONST CHAR8             *CidString;
  CONST CHAR8             *NonBsaHid;

  // Get the _CID and _HID value to write.
  switch (SerialPortInfo->PortSubtype) {
    case EFI_ACPI_DBG2_PORT_SUBTYPE_SERIAL_FULL_16550:
    case EFI_ACPI_DBG2_PORT_SUBTYPE_SERIAL_16550_WITH_GAS:
    {
      // If there is a non-BSA compliant HID, use that.
      NonBsaHid = (CONST CHAR8 *)PcdGetPtr (PcdNonBsaCompliant16550SerialHid);
      if ((NonBsaHid != NULL) && (AsciiStrLen (NonBsaHid) != 0)) {
        if (!(IsValidPnpId (NonBsaHid) || IsValidAcpiId (NonBsaHid))) {
          return EFI_INVALID_PARAMETER;
        }

        HidString = NonBsaHid;
        CidString = "";
      } else {
        HidString = "PNP0501";
        CidString = "PNP0500";
      }

      break;
    }
    case EFI_ACPI_DBG2_PORT_SUBTYPE_SERIAL_ARM_PL011_UART:
    {
      HidString = "ARMH0011";
      CidString = "ARMHB000";
      break;
    }
    case EFI_ACPI_DBG2_PORT_SUBTYPE_SERIAL_ARM_SBSA_GENERIC_UART:
    case EFI_ACPI_DBG2_PORT_SUBTYPE_SERIAL_ARM_SBSA_GENERIC_UART_2X:
    {
      HidString = "ARMHB000";
      CidString = "";
      break;
    }
    default:
    {
      return EFI_INVALID_PARAMETER;
    }
  } // switch

  // Get the _UID NameOp object defined by the "Name ()" statement,
  // and update its value.
  Status = AmlFindNode (
             RootNodeHandle,
             "\\_SB_.COM0._UID",
             &NameOpIdNode
             );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Status = AmlNameOpUpdateInteger (NameOpIdNode, (UINT64)Uid);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  // Get the _HID NameOp object defined by the "Name ()" statement,
  // and update its value.
  Status = AmlFindNode (
             RootNodeHandle,
             "\\_SB_.COM0._HID",
             &NameOpIdNode
             );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Status = AmlNameOpUpdateString (NameOpIdNode, HidString);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  // Get the _CID NameOp object defined by the "Name ()" statement,
  // and update its value.
  Status = AmlFindNode (
             RootNodeHandle,
             "\\_SB_.COM0._CID",
             &NameOpIdNode
             );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  // If we have a CID then update a _CID node else delete the node.
  if (AsciiStrLen (CidString) != 0) {
    Status = AmlNameOpUpdateString (NameOpIdNode, CidString);
  } else {
    // First detach the node from the tree.
    Status = AmlDetachNode (NameOpIdNode);
    if (EFI_ERROR (Status)) {
      return Status;
    }

    // Delete the detached node.
    Status = AmlDeleteTree (NameOpIdNode);
  }

  return Status;
}

/** Fixup the Serial Port _CRS values (BaseAddress, ...).

  @param  [in]  RootNodeHandle  Pointer to the root of an AML tree.
  @param  [in]  SerialPortInfo  Pointer to a Serial Port Information
                                structure.
                                Get the Serial Port Information from there.

  @retval  EFI_SUCCESS            The function completed successfully.
  @retval  EFI_INVALID_PARAMETER  Invalid parameter.
  @retval  EFI_NOT_FOUND          Could not find information.
  @retval  EFI_OUT_OF_RESOURCES   Out of resources.
**/
STATIC
EFI_STATUS
EFIAPI
FixupCrs (
  IN        AML_ROOT_NODE_HANDLE             RootNodeHandle,
  IN  CONST CM_ARCH_COMMON_SERIAL_PORT_INFO  *SerialPortInfo
  )
{
  EFI_STATUS              Status;
  AML_OBJECT_NODE_HANDLE  NameOpCrsNode;
  AML_DATA_NODE_HANDLE    QWordRdNode;

  // Get the "_CRS" object defined by the "Name ()" statement.
  Status = AmlFindNode (
             RootNodeHandle,
             "\\_SB_.COM0._CRS",
             &NameOpCrsNode
             );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  // Get the first Rd node in the "_CRS" object.
  Status = AmlNameOpGetFirstRdNode (NameOpCrsNode, &QWordRdNode);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  if (QWordRdNode == NULL) {
    return EFI_INVALID_PARAMETER;
  }

  // Update the Serial Port base address and length.
  Status = AmlUpdateRdQWord (
             QWordRdNode,
             SerialPortInfo->BaseAddress,
             ((SerialPortInfo->BaseAddressLength < MIN_UART_ADDRESS_LENGTH) ?
              MIN_UART_ADDRESS_LENGTH : SerialPortInfo->BaseAddressLength)
             );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  // Generate an interrupt node as the second Resource Data element in the
  // NameOpCrsNode, if the interrupt for the serial-port is a valid SPI from
  // Table 2-1 in Arm Generic Interrupt Controller Architecture Specification.
  Status = AmlCodeGenRdInterrupt (
             TRUE,                  // Resource Consumer
             FALSE,                 // Level Triggered
             FALSE,                 // Active High
             FALSE,                 // Exclusive
             (UINT32 *)&SerialPortInfo->Interrupt,
             1,
             NameOpCrsNode,
             NULL
             );
  ASSERT_EFI_ERROR (Status);

  return Status;
}

/** Fixup the Serial Port device name.

  @param  [in]  RootNodeHandle  Pointer to the root of an AML tree.
  @param  [in]  SerialPortInfo  Pointer to a Serial Port Information
                                structure.
                                Get the Serial Port Information from there.
  @param  [in]  Name            The Name to give to the Device.
                                Must be a NULL-terminated ASL NameString
                                e.g.: "DEV0", "DV15.DEV0", etc.

  @retval  EFI_SUCCESS            The function completed successfully.
  @retval  EFI_INVALID_PARAMETER  Invalid parameter.
  @retval  EFI_NOT_FOUND          Could not find information.
  @retval  EFI_OUT_OF_RESOURCES   Out of resources.
**/
STATIC
EFI_STATUS
EFIAPI
FixupName (
  IN        AML_ROOT_NODE_HANDLE             RootNodeHandle,
  IN  CONST CM_ARCH_COMMON_SERIAL_PORT_INFO  *SerialPortInfo,
  IN  CONST CHAR8                            *Name
  )
{
  EFI_STATUS              Status;
  AML_OBJECT_NODE_HANDLE  DeviceNode;

  // Get the COM0 variable defined by the "Device ()" statement.
  Status = AmlFindNode (RootNodeHandle, "\\_SB_.COM0", &DeviceNode);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  // Update the Device's name.
  return AmlDeviceOpUpdateName (DeviceNode, Name);
}

/** Fixup the Serial Port Information in the AML tree.

  For each template value:
   - find the node to update;
   - update the value.

  @param  [in]  RootNodeHandle  Pointer to the root of the AML tree.
  @param  [in]  SerialPortInfo  Pointer to a Serial Port Information
                                structure.
                                Get the Serial Port Information from there.
  @param  [in]  Name            The Name to give to the Device.
                                Must be a NULL-terminated ASL NameString
                                e.g.: "DEV0", "DV15.DEV0", etc.
  @param  [in]  Uid             UID for the Serial Port.
  @param  [out] Table           If success, contains the serialized
                                SSDT table.

  @retval  EFI_SUCCESS            The function completed successfully.
  @retval  EFI_INVALID_PARAMETER  Invalid parameter.
  @retval  EFI_NOT_FOUND          Could not find information.
  @retval  EFI_OUT_OF_RESOURCES   Out of resources.
**/
STATIC
EFI_STATUS
EFIAPI
FixupSerialPortInfo (
  IN            AML_ROOT_NODE_HANDLE             RootNodeHandle,
  IN      CONST CM_ARCH_COMMON_SERIAL_PORT_INFO  *SerialPortInfo,
  IN      CONST CHAR8                            *Name,
  IN      CONST UINT64                           Uid,
  OUT       EFI_ACPI_DESCRIPTION_HEADER          **Table
  )
{
  EFI_STATUS  Status;

  ASSERT (RootNodeHandle != NULL);
  ASSERT (SerialPortInfo != NULL);
  ASSERT (Name != NULL);
  ASSERT (Table != NULL);

  // Fixup the _UID, _HID and _CID values.
  Status = FixupIds (RootNodeHandle, Uid, SerialPortInfo);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  // Fixup the _CRS values.
  Status = FixupCrs (RootNodeHandle, SerialPortInfo);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  // Fixup the serial-port name.
  // This MUST be done at the end, otherwise AML paths won't be valid anymore.
  return FixupName (RootNodeHandle, SerialPortInfo, Name);
}

/** Free an SSDT table previously created by
    the BuildSsdtSerialTable function.

  @param [in] Table   Pointer to a SSDT table allocated by
                      the BuildSsdtSerialTable function.

  @retval EFI_SUCCESS           Success.
**/
EFI_STATUS
EFIAPI
FreeSsdtSerialPortTable (
  IN EFI_ACPI_DESCRIPTION_HEADER  *Table
  )
{
  ASSERT (Table != NULL);
  FreePool (Table);
  return EFI_SUCCESS;
}

/** Build a SSDT table describing the input serial port.

  The table created by this function must be freed by FreeSsdtSerialTable.

  @param [in]  AcpiTableInfo    Pointer to the ACPI table information.
  @param [in]  SerialPortInfo   Serial port to describe in the SSDT table.
  @param [in]  Name             The Name to give to the Device.
                                Must be a NULL-terminated ASL NameString
                                e.g.: "DEV0", "DV15.DEV0", etc.
  @param [in]  Uid              UID for the Serial Port.
  @param [out] Table            If success, pointer to the created SSDT table.

  @retval EFI_SUCCESS            Table generated successfully.
  @retval EFI_INVALID_PARAMETER  A parameter is invalid.
  @retval EFI_NOT_FOUND          Could not find information.
  @retval EFI_OUT_OF_RESOURCES   Could not allocate memory.
**/
EFI_STATUS
EFIAPI
BuildSsdtSerialPortTable (
  IN  CONST CM_STD_OBJ_ACPI_TABLE_INFO       *AcpiTableInfo,
  IN  CONST CM_ARCH_COMMON_SERIAL_PORT_INFO  *SerialPortInfo,
  IN  CONST CHAR8                            *Name,
  IN  CONST UINT64                           Uid,
  OUT       EFI_ACPI_DESCRIPTION_HEADER      **Table
  )
{
  EFI_STATUS            Status;
  EFI_STATUS            Status1;
  AML_ROOT_NODE_HANDLE  RootNodeHandle;

  ASSERT (AcpiTableInfo != NULL);
  ASSERT (SerialPortInfo != NULL);
  ASSERT (Name != NULL);
  ASSERT (Table != NULL);

  // Validate the Serial Port Info.
  Status = ValidateSerialPortInfo (SerialPortInfo, 1);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  // Parse the SSDT Serial Port Template.
  Status = AmlParseDefinitionBlock (
             (EFI_ACPI_DESCRIPTION_HEADER *)ssdtserialporttemplate_aml_code,
             &RootNodeHandle
             );
  if (EFI_ERROR (Status)) {
    DEBUG ((
      DEBUG_ERROR,
      "ERROR: SSDT-SERIAL-PORT-FIXUP:"
      " Failed to parse SSDT Serial Port Template. Status = %r\n",
      Status
      ));
    return Status;
  }

  // Fixup the template values.
  Status = FixupSerialPortInfo (
             RootNodeHandle,
             SerialPortInfo,
             Name,
             Uid,
             Table
             );
  if (EFI_ERROR (Status)) {
    DEBUG ((
      DEBUG_ERROR,
      "ERROR: SSDT-SERIAL-PORT-FIXUP: Failed to fixup SSDT Serial Port Table."
      " Status = %r\n",
      Status
      ));
    goto exit_handler;
  }

  // Serialize the tree.
  Status = AmlSerializeDefinitionBlock (
             RootNodeHandle,
             Table
             );
  if (EFI_ERROR (Status)) {
    DEBUG ((
      DEBUG_ERROR,
      "ERROR: SSDT-SERIAL-PORT-FIXUP: Failed to Serialize SSDT Table Data."
      " Status = %r\n",
      Status
      ));
  }

exit_handler:
  // Cleanup
  if (RootNodeHandle != NULL) {
    Status1 = AmlDeleteTree (RootNodeHandle);
    if (EFI_ERROR (Status1)) {
      DEBUG ((
        DEBUG_ERROR,
        "ERROR: SSDT-SERIAL-PORT-FIXUP: Failed to cleanup AML tree."
        " Status = %r\n",
        Status1
        ));
      // If Status was success but we failed to delete the AML Tree
      // return Status1 else return the original error code, i.e. Status.
      if (!EFI_ERROR (Status)) {
        return Status1;
      }
    }
  }

  return Status;
}
