/** @file
  UsbOcUpdateDxeCLX64L.c -- USB Overcurrent Mapping DXE Driver for CLX64L

  Determines the PCH stepping and board revision for the Cooper Lake
  (CLX64L) platform, then publishes the appropriate USB overcurrent
  pin mapping via the UBA (Unified Board Architecture) protocol.

  The driver's main flow:
    1. Standard UEFI driver entry point (process library constructors,
       initialize global services).
    2. Locate the UBA USB OC Update protocol.
    3. Call the protocol with a callback that selects the OC table
       based on PCH stepping.
    4. The OC table selection (UsbOcUpdateGetPchStepping) reads the
       PCH VID/DID register via MMIO PCI config space, decodes the
       stepping, and returns the appropriate pin count.

  Build path: PurleyRpPkg/Uba/UbaMain/Dxe/TypeCLX64L/UsbOcUpdateDxe/

  Copyright (c) Intel Corporation. All rights reserved.
  SPDX-License-Identifier: BSD-2-Clause-Patent
**/

#include <Uefi.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>
#include <Library/DxeServicesTableLib.h>
#include <Library/UefiLib.h>
#include <Library/DxeHobLib.h>
#include <Library/DxePcdLib.h>
#include <Library/DxeMmPciBaseLib.h>
#include <Library/DebugLib.h>
#include <Library/BaseLib.h>
#include <Library/IoLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/PcdLib.h>
#include <Protocol/PciIo.h>
#include "UsbOcUpdateDxeCLX64L.h"

//
// Global Variables
//
EFI_HANDLE           gImageHandle      = NULL;
EFI_SYSTEM_TABLE     *gSystemTable     = NULL;
EFI_BOOT_SERVICES    *gBootServices    = NULL;
EFI_RUNTIME_SERVICES *gRuntimeServices = NULL;
VOID                 *gDs              = NULL;
UINTN                gPchStepping      = PCH_STEPPING_UNKNOWN;  ///< Cached PCH stepping value
VOID                 *gMpciUsra        = NULL;  ///< MM PCI User Space Register Area
VOID                 *gMpcd            = NULL;  ///< PCD protocol pointer
VOID                 *gHobList         = NULL;  ///< HOB list pointer

//
// USB OC Protocol GUIDs
//
EFI_GUID gEfiUsbOcUpdateProtocolGuid = USB_OC_UPDATE_PROTOCOL_GUID;

//
// Forward function declarations
//
EFI_STATUS
UsbOcUpdateCallback (
  IN     VOID   *This,
  OUT    VOID   *OcTableA,
  OUT    VOID   *OcTableB,
  IN     UINTN  TableLength
  );

UINTN
UsbOcUpdateGetPchStepping (
  VOID
  );

/**
  Retrieves the MM PCI user-space register area base address.

  Reads the PCH's MM PCI configuration register (B:D:F 0:0:0,
  offset 0x48h) to obtain the base address for further PCI config
  space access.

  @return  A pointer to the MM PCI User Space Register Area, or
           NULL if the protocol could not be located.

**/
VOID *
GetMpciUsra (
  VOID
  )
{
  UINT64     UsraAddress;
  EFI_STATUS Status;

  //
  // Check if already retrieved
  //
  if (gMpciUsra != NULL) {
    return gMpciUsra;
  }

  //
  // Locate the MM PCI Base protocol
  //
  Status = gBootServices->LocateProtocol (
                            &gEfiMmPciBaseProtocolGuid,
                            NULL,
                            &gMpciUsra
                            );
  if (EFI_ERROR (Status)) {
    gMpciUsra = NULL;
  }

  return gMpciUsra;
}

/**
  Reads a 16-bit value from the PCH's MM PCI configuration space.

  @param[in]  Address  The MM PCI register offset to read.

  @return The 16-bit value read from the specified register.

**/
UINT16
MmPciRead16 (
  IN UINT16  Address
  )
{
  ASSERT ((Address & 1) == 0);
  return *(volatile UINT16 *)((UINTN)gMpciUsra + Address);
}

/**
  Allocates a buffer from EfiBootServicesData pool.

  @param[in]  Size  The number of bytes to allocate.

  @return  A pointer to the allocated buffer, or NULL on failure.

**/
VOID *
AllocatePool (
  IN UINTN  Size
  )
{
  return gBootServices->AllocatePool (EfiBootServicesData, Size, &Size);
}

/**
  Reads a 64-bit unaligned value from a buffer.

  @param[in]  Buffer  Pointer to the buffer to read from.

  @return  The 64-bit value at the given address.

**/
UINT64
ReadUnaligned64 (
  IN CONST VOID  *Buffer
  )
{
  ASSERT (Buffer != NULL);
  return *(UINT64 *)Buffer;
}

/**
  Compares two EFI_GUIDs.

  @param[in]  Guid1  Pointer to the first GUID.
  @param[in]  Guid2  Pointer to the second GUID.

  @return  TRUE if the GUIDs are identical, FALSE otherwise.

**/
BOOLEAN
EFIAPI
CompareGuid (
  IN CONST EFI_GUID  *Guid1,
  IN CONST EFI_GUID  *Guid2
  )
{
  UINT64  Guid1Low;
  UINT64  Guid1High;
  UINT64  Guid2Low;
  UINT64  Guid2High;

  Guid1Low  = ReadUnaligned64 (Guid1);
  Guid2Low  = ReadUnaligned64 (Guid2);
  Guid1High = ReadUnaligned64 ((CONST VOID *)((CONST UINT8 *)Guid1 + 8));
  Guid2High = ReadUnaligned64 ((CONST VOID *)((CONST UINT8 *)Guid2 + 8));

  return (BOOLEAN)(Guid1Low == Guid2Low && Guid1High == Guid2High);
}

/**
  Locates a configuration table entry by GUID in the system table.

  @param[in]   TableGuid  Pointer to the GUID to search for.
  @param[out]  Table      Pointer to receive the located table pointer.

  @return  EFI_SUCCESS           The table was found.
  @return  EFI_NOT_FOUND         No matching table was found.
  @return  EFI_INVALID_PARAMETER TableGuid or Table is NULL.

**/
EFI_STATUS
EFIAPI
EfiGetSystemConfigurationTable (
  IN  EFI_GUID  *TableGuid,
  OUT VOID      **Table
  )
{
  EFI_CONFIGURATION_TABLE  *ConfigTable;
  UINTN                     Index;

  ASSERT (TableGuid != NULL);
  ASSERT (Table != NULL);

  *Table = NULL;
  ConfigTable = (EFI_CONFIGURATION_TABLE *)gSystemTable->ConfigurationTable;

  for (Index = 0; Index < gSystemTable->NumberOfTableEntries; Index++) {
    if (CompareGuid (TableGuid, &ConfigTable->Guid)) {
      *Table = ConfigTable->Table;
      return EFI_SUCCESS;
    }
    ConfigTable++;
  }

  return EFI_NOT_FOUND;
}

/**
  Retrieves the PCH stepping identifier from the LPC device's VID/DID
  register.

  Uses MMIO PCI config access to read the PCH's device ID from
  B:D:F 0:31:0, offset 0x2 (DID register). The device ID encodes
  the PCH stepping, which is decoded as follows:

  DID range         | Stepping
  ------------------+---------
  0xA13F            | LPC Device (no stepping encoded) -> decode via SUBVID
  0xA14C            | LPC Device (alternate DID)
  0xA150-0xA151     | Future stepping range
  (A14C+A14F based) | A0/A1/B0/B1 based on SUBVID
  Other specific    | D0-D3, E0-E1 based on PCH revision

  @return  A PCH_STEPPING_* value identifying the current stepping.

**/
UINTN
UsbOcUpdateGetPchStepping (
  VOID
  )
{
  EFI_STATUS Status;
  VOID       *Usra;
  UINT16     DidValue;
  UINT8      Revision;
  UINT16     SubVid;

  //
  // Use cached value if already computed
  //
  if (gPchStepping != PCH_STEPPING_UNKNOWN) {
    return gPchStepping;
  }

  //
  // Get the MM PCI User Space Register Area
  //
  Usra = GetMpciUsra ();

  //
  // Read the LPC device DID (Device ID) register at offset 0x2
  //
  DidValue = MmPciRead16 ((UINTN)Usra + 2);

  //
  // Decode the PCH stepping from the DID
  //
  if ((DidValue == 0xA13F) ||
      (DidValue >= 0xA150 && DidValue <= 0xA151))
  {
    //
    // Mainstream stepping set (0xA13F base):
    // Read SUBVID from offset 0x2C to distinguish A0/A1/B0/B1
    //
    SubVid = 0;

    Revision = *(UINT8 *)((UINTN)Usra + Usra + 8);  ///< Read PCH revision at +8

    //
    // Read SUBVID at MM PCI offset 0x2C
    //
    SubVid = MmPciRead16 ((UINTN)Usra + 0x2C);

    switch (Revision) {
      case 0x00:
        //
        // DMI Virtual Channel table entry 0 => A0 stepping
        //
        gPchStepping = PCH_STEPPING_A0;
        break;

      case 0x10:
        //
        // DMI Virtual Channel table entry 1 => A1 stepping
        //
        gPchStepping = PCH_STEPPING_A1;
        break;

      case 0x20:
        //
        // DMI Virtual Channel table entry 2 => B0 stepping
        //
        gPchStepping = PCH_STEPPING_B0;
        break;

      case 0x30:
      case 0x31:
        //
        // DMI Virtual Channel table entry 3 => B1 stepping
        //
        gPchStepping = PCH_STEPPING_B1;
        break;

      default:
        //
        // Fall through to extended stepping decode
        //
        break;
    }

    //
    // If we resolved to a known stepping via SUBVID, cache and return
    //
    if (gPchStepping != PCH_STEPPING_UNKNOWN) {
      return gPchStepping;
    }

    //
    // Extended stepping range for B2/C0/C1:
    // DID plus 0x62A0 -> 0x62A8 range indicates B2/C stepping
    //
    if ((DidValue + 0x62A0) <= 8) {
      UINTN ExtendedStep;

      ExtendedStep = 0;
      //
      // B2 = 35, B3 = 36, C0 = 37, C1 = 38
      //
      if (Revision == 0x10) {
        gPchStepping = PCH_STEPPING_B2;
      } else if (Revision == 0x11) {
        gPchStepping = PCH_STEPPING_B3;
      } else if (Revision == 0x20) {
        gPchStepping = PCH_STEPPING_C0;
      } else if (Revision == 0x21) {
        gPchStepping = PCH_STEPPING_C1;
      } else {
        gPchStepping = PCH_STEPPING_UNKNOWN;
      }
    } else {
      gPchStepping = PCH_STEPPING_UNKNOWN;
    }
  }
  else if (DidValue == 0xA14C)
  {
    //
    // Alternate DID (0xA14C) — check revision for stepping decode
    //
    Revision = *(UINT8 *)((UINTN)Usra + 8);

    if (*(UINT8 *)((UINTN)gMpcd + 252) == 4) {
      //
      // PCD indicates Server platform: stepping = D0
      // No further decode needed
      //
      gPchStepping = PCH_STEPPING_D0;
    } else {
      //
      // Not Server platform: stepping depends on revision
      //
      switch (Revision) {
        case 2:
          gPchStepping = PCH_STEPPING_D1;
          break;
        case 3:
          gPchStepping = PCH_STEPPING_D2;
          break;
        case 4:
          gPchStepping = PCH_STEPPING_D3;
          break;
        case 8:
          gPchStepping = PCH_STEPPING_E0;
          break;
        case 9:
          gPchStepping = PCH_STEPPING_E1;
          break;
        default:
          DEBUG ((
            DEBUG_ERROR,
            "Unsupported PCH Stepping. Supporting PCH stepping starting from %s and above\n",
            "LbgA0"
            ));
          ASSERT (FALSE);
          gPchStepping = PCH_STEPPING_UNKNOWN;
          break;
      }
    }
  }
  else
  {
    //
    // Unknown DID — stepping not recognized
    //
    gPchStepping = PCH_STEPPING_UNKNOWN;
  }

  return gPchStepping;
}

/**
  USB OC callback — selects the appropriate OC table based on
  PCH stepping.

  This function is called by the UBA infrastructure. It determines
  the correct number of USB OC pins for the current PCH stepping
  and returns the appropriate OC mapping table.

  @param[in]   This         Pointer to the USB OC Update protocol.
  @param[out]  OcTableA     Pointer to receive OC table A.
  @param[out]  OcTableB     Pointer to receive OC table B.
  @param[in]   TableLength  Length of table entries.

  @return  EFI_SUCCESS  The OC tables were populated.

**/
EFI_STATUS
UsbOcUpdateCallback (
  IN     VOID   *This,
  OUT    VOID   *OcTableA,
  OUT    VOID   *OcTableB,
  IN     UINTN  TableLength
  )
{
  UINTN  Stepping;
  UINTN  OcPinCount;

  Stepping = UsbOcUpdateGetPchStepping ();

  //
  // Choose OC table based on stepping
  // Stepping >= 48 (D0 and above) uses 48-pin OC mapping
  // All others use default 54-pin mapping
  //
  if (Stepping >= PCH_STEPPING_D0) {
    OcPinCount = USB_OC_PIN_COUNT_48;
  } else {
    OcPinCount = USB_OC_PIN_COUNT_54;
  }

  //
  // Populate output structures
  // The actual OC table data is provided by the UBA base protocol
  // (not defined in this driver) and filled in via the protocol
  // dispatch layer.
  //
  *OcTableA = NULL;
  *OcTableB = NULL;

  return EFI_SUCCESS;
}

/**
  Driver entry point.

  Initializes UEFI boot/runtime services, locates the UBA USB OC
  Update protocol, and registers the OC mapping callback.

  @param[in]  ImageHandle  The firmware-allocated handle for this image.
  @param[in]  SystemTable  Pointer to the UEFI system table.

  @return  EFI_SUCCESS  The driver initialized successfully.
  @return  Others       An error occurred during initialization.

**/
EFI_STATUS
EFIAPI
ModuleEntryPoint (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS           Status;
  USB_OC_UPDATE_PROTOCOL  *UsbOcProtocol;

  //
  // Initialize library services
  //
  ProcessLibraryConstructorList (ImageHandle, SystemTable);

  //
  // Log the driver entry
  //
  DEBUG ((DEBUG_INFO, "UBA:UsbOcUpdate-TypeClx64L\n"));

  //
  // Locate the UBA USB OC Update protocol
  //
  Status = gBootServices->LocateProtocol (
                            &gEfiUsbOcUpdateProtocolGuid,
                            NULL,
                            (VOID **)&UsbOcProtocol
                            );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  //
  // Register the USB OC mapping callback
  //
  Status = UsbOcProtocol->GetUsbOcMapping (
                            UsbOcProtocol,
                            NULL,
                            NULL,
                            16
                            );
  return Status;
}