/** @file
  Arm Serial Port Parser.

  Copyright (c) 2021 - 2023, Arm Limited. All rights reserved.<BR>
  SPDX-License-Identifier: BSD-2-Clause-Patent

  @par Reference(s):
  - linux/Documentation/devicetree/bindings/serial/serial.yaml
  - linux/Documentation/devicetree/bindings/serial/8250.txt
  - linux/Documentation/devicetree/bindings/serial/arm_sbsa_uart.txt
  - linux/Documentation/devicetree/bindings/serial/pl011.yaml
**/

#include <IndustryStandard/DebugPort2Table.h>

#include "CmObjectDescUtility.h"
#include "FdtHwInfoParser.h"
#include "Serial/ArmSerialPortParser.h"

/** List of "compatible" property values for serial port nodes.

  Any other "compatible" value is not supported by this module.
*/
STATIC CONST COMPATIBILITY_STR  SerialCompatibleStr[] = {
  { "ns16550a"      },
  { "arm,sbsa-uart" },
  { "arm,pl011"     }
};

/** COMPATIBILITY_INFO structure for the SerialCompatible.
*/
CONST COMPATIBILITY_INFO  SerialCompatibleInfo = {
  ARRAY_SIZE (SerialCompatibleStr),
  SerialCompatibleStr
};

/** 16550 UART compatible strings.

  Any string of this list must be part of SerialCompatible.
*/
STATIC CONST COMPATIBILITY_STR  Serial16550CompatibleStr[] = {
  { "ns16550a" }
};

/** COMPATIBILITY_INFO structure for the Serial16550Compatible.
*/
CONST COMPATIBILITY_INFO  Serial16550CompatibleInfo = {
  ARRAY_SIZE (Serial16550CompatibleStr),
  Serial16550CompatibleStr
};

/** SBSA UART compatible strings.

  Include PL011 as SBSA uart is a subset of PL011.

  Any string of this list must be part of SerialCompatible.
*/
STATIC CONST COMPATIBILITY_STR  SerialSbsaCompatibleStr[] = {
  { "arm,sbsa-uart" },
  { "arm,pl011"     }
};

/** COMPATIBILITY_INFO structure for the SerialSbsaCompatible.
*/
CONST COMPATIBILITY_INFO  SerialSbsaCompatibleInfo = {
  ARRAY_SIZE (SerialSbsaCompatibleStr),
  SerialSbsaCompatibleStr
};

/** Parse a serial port node.

  @param [in]  Fdt               Pointer to a Flattened Device Tree (Fdt).
  @param [in]  SerialPortNode    Offset of a serial-port node.
  @param [in]  SerialPortInfo    The CM_ARM_SERIAL_PORT_INFO to populate.

  @retval EFI_SUCCESS             The function completed successfully.
  @retval EFI_ABORTED             An error occurred.
  @retval EFI_INVALID_PARAMETER   Invalid parameter.
  @retval EFI_UNSUPPORTED         Unsupported.
**/
STATIC
EFI_STATUS
EFIAPI
SerialPortNodeParser (
  IN  CONST VOID               *Fdt,
  IN  INT32                    SerialPortNode,
  IN  CM_ARM_SERIAL_PORT_INFO  *SerialPortInfo
  )
{
  EFI_STATUS   Status;
  INT32        IntcNode;
  CONST UINT8  *SizeValue;

  INT32  AddressCells;
  INT32  SizeCells;
  INT32  IntCells;

  CONST UINT8  *Data;
  INT32        DataSize;
  UINT8        AccessSize;

  if ((Fdt == NULL) ||
      (SerialPortInfo == NULL))
  {
    ASSERT (0);
    return EFI_INVALID_PARAMETER;
  }

  Status = FdtGetParentAddressInfo (
             Fdt,
             SerialPortNode,
             &AddressCells,
             &SizeCells
             );
  if (EFI_ERROR (Status)) {
    ASSERT (0);
    return Status;
  }

  // Don't support more than 64 bits and less than 32 bits addresses.
  if ((AddressCells < 1)  ||
      (AddressCells > 2)  ||
      (SizeCells < 1)     ||
      (SizeCells > 2))
  {
    ASSERT (0);
    return EFI_ABORTED;
  }

  Data = fdt_getprop (Fdt, SerialPortNode, "reg", &DataSize);
  if ((Data == NULL) ||
      (DataSize < (INT32)(sizeof (UINT32) *
                          GET_DT_REG_ADDRESS_OFFSET (1, AddressCells, SizeCells)) - 1))
  {
    // If error or not enough space.
    ASSERT (0);
    return EFI_ABORTED;
  }

  if (AddressCells == 2) {
    SerialPortInfo->BaseAddress = fdt64_to_cpu (*(UINT64 *)Data);
  } else {
    SerialPortInfo->BaseAddress = fdt32_to_cpu (*(UINT32 *)Data);
  }

  SizeValue = Data + (sizeof (UINT32) *
                      GET_DT_REG_SIZE_OFFSET (0, AddressCells, SizeCells));
  if (SizeCells == 2) {
    SerialPortInfo->BaseAddressLength = fdt64_to_cpu (*(UINT64 *)SizeValue);
  } else {
    SerialPortInfo->BaseAddressLength = fdt32_to_cpu (*(UINT32 *)SizeValue);
  }

  // Get the associated interrupt-controller.
  Status = FdtGetIntcParentNode (Fdt, SerialPortNode, &IntcNode);
  if (EFI_ERROR (Status)) {
    ASSERT (0);
    if (Status == EFI_NOT_FOUND) {
      // Should have found the node.
      Status = EFI_ABORTED;
    }

    return Status;
  }

  // Get the number of cells used to encode an interrupt.
  Status = FdtGetInterruptCellsInfo (Fdt, IntcNode, &IntCells);
  if (EFI_ERROR (Status)) {
    ASSERT (0);
    return Status;
  }

  Data = fdt_getprop (Fdt, SerialPortNode, "interrupts", &DataSize);
  if ((Data == NULL) || (DataSize != (IntCells * sizeof (UINT32)))) {
    // If error or not 1 interrupt.
    ASSERT (0);
    return EFI_ABORTED;
  }

  SerialPortInfo->Interrupt = FdtGetInterruptId ((CONST UINT32 *)Data);

  // Note: clock-frequency is optional for SBSA UART.
  Data = fdt_getprop (Fdt, SerialPortNode, "clock-frequency", &DataSize);
  if (Data != NULL) {
    if (DataSize < sizeof (UINT32)) {
      // If error or not enough space.
      ASSERT (0);
      return EFI_ABORTED;
    } else if (fdt_node_offset_by_phandle (Fdt, fdt32_to_cpu (*Data)) >= 0) {
      // "clock-frequency" can be a "clocks phandle to refer to the clk used".
      // This is not supported.
      ASSERT (0);
      return EFI_UNSUPPORTED;
    }

    SerialPortInfo->Clock = fdt32_to_cpu (*(UINT32 *)Data);
  }

  if (FdtNodeIsCompatible (Fdt, SerialPortNode, &Serial16550CompatibleInfo)) {
    SerialPortInfo->PortSubtype =
      EFI_ACPI_DBG2_PORT_SUBTYPE_SERIAL_16550_WITH_GAS;

    /* reg-io-width:
         description: |
         The size (in bytes) of the IO accesses that should be performed on the
         device. There are some systems that require 32-bit accesses to the
         UART.
    */
    Data = fdt_getprop (Fdt, SerialPortNode, "reg-io-width", &DataSize);
    if (Data != NULL) {
      if (DataSize < sizeof (UINT32)) {
        // If error or not enough space.
        ASSERT (0);
        return EFI_ABORTED;
      }

      AccessSize = fdt32_to_cpu (*(UINT32 *)Data);
      if (AccessSize > EFI_ACPI_6_3_QWORD) {
        ASSERT (0);
        return EFI_INVALID_PARAMETER;
      }

      SerialPortInfo->AccessSize = AccessSize;
    } else {
      // 8250/16550 defaults to byte access.
      SerialPortInfo->AccessSize = EFI_ACPI_6_3_BYTE;
    }
  } else if (FdtNodeIsCompatible (
               Fdt,
               SerialPortNode,
               &SerialSbsaCompatibleInfo
               ))
  {
    SerialPortInfo->PortSubtype =
      EFI_ACPI_DBG2_PORT_SUBTYPE_SERIAL_ARM_SBSA_GENERIC_UART;
  } else {
    ASSERT (0);
    return EFI_UNSUPPORTED;
  }

  // Set Baudrate to 115200 by default
  SerialPortInfo->BaudRate = 115200;
  return EFI_SUCCESS;
}

/** Find the console serial-port node in the DT.

  This function fetches the node referenced in the "stdout-path"
  property of the "chosen" node.

  @param [in]  Fdt                  Pointer to a Flattened Device Tree (Fdt).
  @param [out] SerialConsoleNode    If success, contains the node offset
                                    of the console serial-port node.

  @retval EFI_SUCCESS             The function completed successfully.
  @retval EFI_ABORTED             An error occurred.
  @retval EFI_INVALID_PARAMETER   Invalid parameter.
  @retval EFI_NOT_FOUND           Not found.
**/
STATIC
EFI_STATUS
EFIAPI
GetSerialConsoleNode (
  IN  CONST VOID   *Fdt,
  OUT       INT32  *SerialConsoleNode
  )
{
  CONST CHAR8  *Prop;
  INT32        PropSize;
  CONST CHAR8  *Path;
  INT32        PathLen;
  INT32        ChosenNode;

  if ((Fdt == NULL) ||
      (SerialConsoleNode == NULL))
  {
    ASSERT (0);
    return EFI_INVALID_PARAMETER;
  }

  // The "chosen" node resides at the root of the DT. Fetch it.
  ChosenNode = fdt_path_offset (Fdt, "/chosen");
  if (ChosenNode < 0) {
    return EFI_NOT_FOUND;
  }

  Prop = fdt_getprop (Fdt, ChosenNode, "stdout-path", &PropSize);
  if ((Prop == NULL) || (PropSize < 0)) {
    return EFI_NOT_FOUND;
  }

  // Determine the actual path length, as a colon terminates the path.
  Path = ScanMem8 (Prop, PropSize, ':');
  if (Path == NULL) {
    PathLen = (UINT32)AsciiStrLen (Prop);
  } else {
    PathLen = (INT32)(Path - Prop);
  }

  // Aliases cannot start with a '/', so it must be the actual path.
  if (Prop[0] == '/') {
    *SerialConsoleNode = fdt_path_offset_namelen (Fdt, Prop, PathLen);
    return EFI_SUCCESS;
  }

  // Lookup the alias, as this contains the actual path.
  Path = fdt_get_alias_namelen (Fdt, Prop, PathLen);
  if (Path == NULL) {
    return EFI_NOT_FOUND;
  }

  *SerialConsoleNode = fdt_path_offset (Fdt, Path);
  return EFI_SUCCESS;
}

/** CM_ARM_SERIAL_PORT_INFO dispatcher function (for a generic serial-port).

  @param [in]  FdtParserHandle A handle to the parser instance.
  @param [in]  GenericSerialInfo  Pointer to a serial port info list.
  @param [in]  NodeCount          Count of serial ports to dispatch.
  @param [in]  SerialObjectId     Serial port object ID.

  @retval EFI_SUCCESS             The function completed successfully.
  @retval EFI_ABORTED             An error occurred.
  @retval EFI_INVALID_PARAMETER   Invalid parameter.
  @retval EFI_NOT_FOUND           Not found.
  @retval EFI_UNSUPPORTED         Unsupported.
**/
STATIC
EFI_STATUS
EFIAPI
ArmSerialPortInfoDispatch (
  IN  CONST FDT_HW_INFO_PARSER_HANDLE  FdtParserHandle,
  IN  CM_ARM_SERIAL_PORT_INFO          *GenericSerialInfo,
  IN  INT32                            NodeCount,
  IN  EARM_OBJECT_ID                   SerialObjectId
  )
{
  EFI_STATUS         Status;
  CM_OBJ_DESCRIPTOR  *NewCmObjDesc;

  if ((GenericSerialInfo == NULL) || (NodeCount == 0)) {
    ASSERT (0);
    return EFI_INVALID_PARAMETER;
  }

  if ((SerialObjectId != EArmObjSerialPortInfo) &&
      (SerialObjectId != EArmObjSerialDebugPortInfo) &&
      (SerialObjectId != EArmObjSerialConsolePortInfo))
  {
    ASSERT (0);
    return EFI_INVALID_PARAMETER;
  }

  // Dispatch the Generic Serial ports
  Status = CreateCmObjDesc (
             CREATE_CM_ARM_OBJECT_ID (SerialObjectId),
             NodeCount,
             GenericSerialInfo,
             sizeof (CM_ARM_SERIAL_PORT_INFO) * NodeCount,
             &NewCmObjDesc
             );
  if (EFI_ERROR (Status)) {
    ASSERT (0);
    return Status;
  }

  // Add all the CmObjs to the Configuration Manager.
  Status = AddMultipleCmObj (FdtParserHandle, NewCmObjDesc, 0, NULL);
  ASSERT_EFI_ERROR (Status);
  FreeCmObjDesc (NewCmObjDesc);
  return Status;
}

/** CM_ARM_SERIAL_PORT_INFO parser function (for debug/console serial-port).

  This parser expects FdtBranch to be the debug serial-port node.
  At most one CmObj is created.
  The following structure is populated:
  typedef struct CmArmSerialPortInfo {
    UINT64  BaseAddress;                      // {Populated}
    UINT32  Interrupt;                        // {Populated}
    UINT64  BaudRate;                         // {default}
    UINT32  Clock;                            // {Populated}
    UINT16  PortSubtype;                      // {Populated}
    UINT64  BaseAddressLength                 // {Populated}
  } CM_ARM_SERIAL_PORT_INFO;

  A parser parses a Device Tree to populate a specific CmObj type. None,
  one or many CmObj can be created by the parser.
  The created CmObj are then handed to the parser's caller through the
  HW_INFO_ADD_OBJECT interface.
  This can also be a dispatcher. I.e. a function that not parsing a
  Device Tree but calling other parsers.

  @param [in]  FdtParserHandle A handle to the parser instance.
  @param [in]  FdtBranch       When searching for DT node name, restrict
                               the search to this Device Tree branch.
  @param [in]  SerialObjectId  ArmNamespace Object ID for the serial port.

  @retval EFI_SUCCESS             The function completed successfully.
  @retval EFI_ABORTED             An error occurred.
  @retval EFI_INVALID_PARAMETER   Invalid parameter.
  @retval EFI_NOT_FOUND           Not found.
  @retval EFI_UNSUPPORTED         Unsupported.
**/
STATIC
EFI_STATUS
EFIAPI
ArmSerialPortInfoParser (
  IN  CONST FDT_HW_INFO_PARSER_HANDLE  FdtParserHandle,
  IN        INT32                      FdtBranch,
  IN        EARM_OBJECT_ID             SerialObjectId
  )
{
  EFI_STATUS               Status;
  CM_ARM_SERIAL_PORT_INFO  SerialInfo;

  if ((SerialObjectId != EArmObjSerialDebugPortInfo) &&
      (SerialObjectId != EArmObjSerialConsolePortInfo))
  {
    ASSERT (0);
    return EFI_INVALID_PARAMETER;
  }

  ZeroMem (&SerialInfo, sizeof (SerialInfo));

  Status = SerialPortNodeParser (
             FdtParserHandle->Fdt,
             FdtBranch,
             &SerialInfo
             );
  if (EFI_ERROR (Status)) {
    ASSERT (0);
    return Status;
  }

  Status = ArmSerialPortInfoDispatch (
             FdtParserHandle,
             &SerialInfo,
             1,
             SerialObjectId
             );
  ASSERT_EFI_ERROR (Status);
  return Status;
}

/** SerialPort dispatcher.

  This disptacher populates the CM_ARM_SERIAL_PORT_INFO structure for
  the following CM_OBJ_ID:
   - EArmObjSerialConsolePortInfo
   - EArmObjSerialDebugPortInfo
   - EArmObjSerialPortInfo

  A parser parses a Device Tree to populate a specific CmObj type. None,
  one or many CmObj can be created by the parser.
  The created CmObj are then handed to the parser's caller through the
  HW_INFO_ADD_OBJECT interface.
  This can also be a dispatcher. I.e. a function that not parsing a
  Device Tree but calling other parsers.

  @param [in]  FdtParserHandle A handle to the parser instance.
  @param [in]  FdtBranch       When searching for DT node name, restrict
                               the search to this Device Tree branch.

  @retval EFI_SUCCESS             The function completed successfully.
  @retval EFI_ABORTED             An error occurred.
  @retval EFI_INVALID_PARAMETER   Invalid parameter.
  @retval EFI_NOT_FOUND           Not found.
  @retval EFI_UNSUPPORTED         Unsupported.
**/
EFI_STATUS
EFIAPI
SerialPortDispatcher (
  IN  CONST FDT_HW_INFO_PARSER_HANDLE  FdtParserHandle,
  IN        INT32                      FdtBranch
  )
{
  EFI_STATUS               Status;
  INT32                    SerialConsoleNode;
  INT32                    SerialDebugNode;
  INT32                    SerialNode;
  UINT32                   Index;
  UINT32                   SerialNodeCount;
  UINT32                   SerialNodesRemaining;
  CM_ARM_SERIAL_PORT_INFO  *GenericSerialInfo;
  UINT32                   GenericSerialIndex;
  VOID                     *Fdt;

  if (FdtParserHandle == NULL) {
    ASSERT (0);
    return EFI_INVALID_PARAMETER;
  }

  Fdt = FdtParserHandle->Fdt;

  // Count the number of serial-ports.
  Status = FdtCountCompatNodeInBranch (
             Fdt,
             FdtBranch,
             &SerialCompatibleInfo,
             &SerialNodeCount
             );
  if (EFI_ERROR (Status)) {
    ASSERT (0);
    return Status;
  }

  if (SerialNodeCount == 0) {
    return EFI_NOT_FOUND;
  }

  // Track remaining nodes separately as SerialNodeCount
  // is used in for loop below and reducing SerialNodeCount
  // would result in the Generic Serial port nodes not
  // being found if the serial console port node is among
  // the first few serial nodes.
  SerialNodesRemaining = SerialNodeCount;

  // Identify the serial console port.
  Status = GetSerialConsoleNode (Fdt, &SerialConsoleNode);
  if (Status == EFI_NOT_FOUND) {
    // No serial console.
    SerialConsoleNode = -1;
  } else if (EFI_ERROR (Status)) {
    ASSERT (0);
    return Status;
  } else {
    // Parse the console serial-port.
    Status = ArmSerialPortInfoParser (
               FdtParserHandle,
               SerialConsoleNode,
               EArmObjSerialConsolePortInfo
               );
    if (EFI_ERROR (Status)) {
      ASSERT (0);
      return Status;
    }

    SerialNodesRemaining--;
  }

  GenericSerialInfo = NULL;
  if (SerialNodesRemaining > 1) {
    // We have more than one serial port remaining.
    // This means that the first serial port will
    // be reserved as a debug port, and the remaining
    // will be for general purpose use.
    SerialNodesRemaining--;
    GenericSerialInfo = AllocateZeroPool (
                          SerialNodesRemaining *
                          sizeof (CM_ARM_SERIAL_PORT_INFO)
                          );
    if (GenericSerialInfo == NULL) {
      ASSERT (0);
      return EFI_OUT_OF_RESOURCES;
    }
  }

  SerialNode         = FdtBranch;
  SerialDebugNode    = -1;
  GenericSerialIndex = 0;
  for (Index = 0; Index < SerialNodeCount; Index++) {
    // Search the next serial-port node in the branch.
    Status = FdtGetNextCompatNodeInBranch (
               Fdt,
               FdtBranch,
               &SerialCompatibleInfo,
               &SerialNode
               );
    if (EFI_ERROR (Status)) {
      ASSERT (0);
      if (Status == EFI_NOT_FOUND) {
        // Should have found the node.
        Status = EFI_ABORTED;
      }

      goto exit_handler;
    }

    // Ignore the serial console node.
    if (SerialNode == SerialConsoleNode) {
      continue;
    } else if (SerialDebugNode == -1) {
      // The first serial-port node, not being the console serial-port,
      // will be the debug serial-port.
      SerialDebugNode = SerialNode;
      Status          = ArmSerialPortInfoParser (
                          FdtParserHandle,
                          SerialDebugNode,
                          EArmObjSerialDebugPortInfo
                          );
      if (EFI_ERROR (Status)) {
        ASSERT (0);
        goto exit_handler;
      }
    } else {
      if (GenericSerialInfo == NULL) {
        // Should not be possible.
        ASSERT (0);
        Status = EFI_ABORTED;
        goto exit_handler;
      }

      Status = SerialPortNodeParser (
                 Fdt,
                 SerialNode,
                 &GenericSerialInfo[GenericSerialIndex++]
                 );
      if (EFI_ERROR (Status)) {
        ASSERT (0);
        goto exit_handler;
      }
    }
  } // for

  if (GenericSerialIndex > 0) {
    Status = ArmSerialPortInfoDispatch (
               FdtParserHandle,
               GenericSerialInfo,
               GenericSerialIndex,
               EArmObjSerialPortInfo
               );
  }

exit_handler:
  if (GenericSerialInfo != NULL) {
    FreePool (GenericSerialInfo);
  }

  return Status;
}
