/** @file
  Instance of Runtime PCI Segment Library that support multi-segment PCI configuration access.

  PCI Segment Library that consumes segment information provided by PciSegmentInfoLib to
   support multi-segment PCI configuration access through enhanced configuration access mechanism.

  Copyright (c) 2017, Intel Corporation. All rights reserved.<BR>
  SPDX-License-Identifier: BSD-2-Clause-Patent

**/

#include "PciSegmentLibCommon.h"
#include <PiDxe.h>
#include <Guid/EventGroup.h>
#include <Library/UefiRuntimeLib.h>
#include <Library/DxeServicesTableLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/PciSegmentInfoLib.h>

///
/// Define table for mapping PCI Segment MMIO physical addresses to virtual addresses at OS runtime
///
typedef struct {
  UINTN  PhysicalAddress;
  UINTN  VirtualAddress;
} PCI_SEGMENT_RUNTIME_REGISTRATION_TABLE;

///
/// Set Virtual Address Map Event
///
EFI_EVENT                               mDxeRuntimePciSegmentLibVirtualNotifyEvent = NULL;

///
/// The number of PCI devices that have been registered for runtime access.
///
UINTN                                   mDxeRuntimePciSegmentLibNumberOfRuntimeRanges = 0;

///
/// The table of PCI devices that have been registered for runtime access.
///
PCI_SEGMENT_RUNTIME_REGISTRATION_TABLE  *mDxeRuntimePciSegmentLibRegistrationTable = NULL;

///
/// The table index of the most recent virtual address lookup.
///
UINTN                                   mDxeRuntimePciSegmentLibLastRuntimeRange = 0;

/**
  Convert the physical PCI Express MMIO addresses for all registered PCI devices
  to virtual addresses.

  @param[in]    Event   The event that is being processed.
  @param[in]    Context The Event Context.
**/
VOID
EFIAPI
DxeRuntimePciSegmentLibVirtualNotify (
  IN EFI_EVENT  Event,
  IN VOID       *Context
  )
{
  UINTN         Index;
  EFI_STATUS    Status;

  //
  // If there have been no runtime registrations, then just return
  //
  if (mDxeRuntimePciSegmentLibRegistrationTable == NULL) {
    return;
  }

  //
  // Convert physical addresses associated with the set of registered PCI devices to
  // virtual addresses.
  //
  for (Index = 0; Index < mDxeRuntimePciSegmentLibNumberOfRuntimeRanges; Index++) {
    Status = EfiConvertPointer (0, (VOID **) &(mDxeRuntimePciSegmentLibRegistrationTable[Index].VirtualAddress));
    ASSERT_EFI_ERROR (Status);
  }

  //
  // Convert table pointer that is allocated from EfiRuntimeServicesData to a virtual address.
  //
  Status = EfiConvertPointer (0, (VOID **) &mDxeRuntimePciSegmentLibRegistrationTable);
  ASSERT_EFI_ERROR (Status);
}

/**
  The constructor function caches the PCI Express Base Address and creates a
  Set Virtual Address Map event to convert physical address to virtual addresses.

  @param  ImageHandle   The firmware allocated handle for the EFI image.
  @param  SystemTable   A pointer to the EFI System Table.

  @retval EFI_SUCCESS   The constructor completed successfully.
  @retval Other value   The constructor did not complete successfully.

**/
EFI_STATUS
EFIAPI
DxeRuntimePciSegmentLibConstructor (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS  Status;

  //
  // Register SetVirtualAddressMap () notify function
  //
  Status = gBS->CreateEventEx (
                  EVT_NOTIFY_SIGNAL,
                  TPL_NOTIFY,
                  DxeRuntimePciSegmentLibVirtualNotify,
                  NULL,
                  &gEfiEventVirtualAddressChangeGuid,
                  &mDxeRuntimePciSegmentLibVirtualNotifyEvent
                  );
  ASSERT_EFI_ERROR (Status);

  return Status;
}

/**
  The destructor function frees any allocated buffers and closes the Set Virtual
  Address Map event.

  @param  ImageHandle   The firmware allocated handle for the EFI image.
  @param  SystemTable   A pointer to the EFI System Table.

  @retval EFI_SUCCESS   The destructor completed successfully.
  @retval Other value   The destructor did not complete successfully.

**/
EFI_STATUS
EFIAPI
DxeRuntimePciSegmentLibDestructor (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS  Status;

  //
  // If one or more PCI devices have been registered for runtime access, then
  // free the registration table.
  //
  if (mDxeRuntimePciSegmentLibRegistrationTable != NULL) {
    FreePool (mDxeRuntimePciSegmentLibRegistrationTable);
  }

  //
  // Close the Set Virtual Address Map event
  //
  Status = gBS->CloseEvent (mDxeRuntimePciSegmentLibVirtualNotifyEvent);
  ASSERT_EFI_ERROR (Status);

  return Status;
}

/**
  Register a PCI device so PCI configuration registers may be accessed after
  SetVirtualAddressMap().

  If any reserved bits in Address are set, then ASSERT().

  @param  Address The address that encodes the PCI Bus, Device, Function and
                  Register.

  @retval RETURN_SUCCESS           The PCI device was registered for runtime access.
  @retval RETURN_UNSUPPORTED       An attempt was made to call this function
                                   after ExitBootServices().
  @retval RETURN_UNSUPPORTED       The resources required to access the PCI device
                                   at runtime could not be mapped.
  @retval RETURN_OUT_OF_RESOURCES  There are not enough resources available to
                                   complete the registration.

**/
RETURN_STATUS
EFIAPI
PciSegmentRegisterForRuntimeAccess (
  IN UINTN  Address
  )
{
  RETURN_STATUS                    Status;
  EFI_GCD_MEMORY_SPACE_DESCRIPTOR  Descriptor;
  UINTN                            Index;
  VOID                             *NewTable;
  UINTN                            Count;
  PCI_SEGMENT_INFO                 *SegmentInfo;
  UINT64                           EcamAddress;

  //
  // Convert Address to a ECAM address at the beginning of the PCI Configuration
  // header for the specified PCI Bus/Dev/Func
  //
  Address &= ~(UINTN)EFI_PAGE_MASK;
  SegmentInfo = GetPciSegmentInfo (&Count);
  EcamAddress = PciSegmentLibGetEcamAddress (Address, SegmentInfo, Count);

  //
  // Return an error if this function is called after ExitBootServices().
  //
  if (EfiAtRuntime ()) {
    return RETURN_UNSUPPORTED;
  }
  if (sizeof (UINTN) == sizeof (UINT32)) {
    ASSERT (EcamAddress < BASE_4GB);
  }
  Address = (UINTN)EcamAddress;

  //
  // See if Address has already been registerd for runtime access
  //
  for (Index = 0; Index < mDxeRuntimePciSegmentLibNumberOfRuntimeRanges; Index++) {
    if (mDxeRuntimePciSegmentLibRegistrationTable[Index].PhysicalAddress == Address) {
      return RETURN_SUCCESS;
    }
  }

  //
  // Get the GCD Memory Descriptor for the ECAM Address
  //
  Status = gDS->GetMemorySpaceDescriptor (Address, &Descriptor);
  if (EFI_ERROR (Status)) {
    return RETURN_UNSUPPORTED;
  }

  //
  // Mark the 4KB region for the PCI Express Bus/Dev/Func as EFI_RUNTIME_MEMORY so the OS
  // will allocate a virtual address range for the 4KB PCI Configuration Header.
  //
  Status = gDS->SetMemorySpaceAttributes (Address, EFI_PAGE_SIZE, Descriptor.Attributes | EFI_MEMORY_RUNTIME);
  if (EFI_ERROR (Status)) {
    return RETURN_UNSUPPORTED;
  }

  //
  // Grow the size of the registration table
  //
  NewTable = ReallocateRuntimePool (
               (mDxeRuntimePciSegmentLibNumberOfRuntimeRanges + 0) * sizeof (PCI_SEGMENT_RUNTIME_REGISTRATION_TABLE),
               (mDxeRuntimePciSegmentLibNumberOfRuntimeRanges + 1) * sizeof (PCI_SEGMENT_RUNTIME_REGISTRATION_TABLE),
               mDxeRuntimePciSegmentLibRegistrationTable
               );
  if (NewTable == NULL) {
    return RETURN_OUT_OF_RESOURCES;
  }
  mDxeRuntimePciSegmentLibRegistrationTable = NewTable;
  mDxeRuntimePciSegmentLibRegistrationTable[mDxeRuntimePciSegmentLibNumberOfRuntimeRanges].PhysicalAddress = Address;
  mDxeRuntimePciSegmentLibRegistrationTable[mDxeRuntimePciSegmentLibNumberOfRuntimeRanges].VirtualAddress  = Address;
  mDxeRuntimePciSegmentLibNumberOfRuntimeRanges++;

  return RETURN_SUCCESS;
}

/**
  Return the linear address for the physical address.

  @param  Address  The physical address.

  @retval The linear address.
**/
UINTN
PciSegmentLibVirtualAddress (
  IN UINTN                     Address
  )
{
  UINTN                        Index;
  //
  // If SetVirtualAddressMap() has not been called, then just return the physical address
  //
  if (!EfiGoneVirtual ()) {
    return Address;
  }

  //
  // See if there is a physical address match at the exact same index as the last address match
  //
  if (mDxeRuntimePciSegmentLibRegistrationTable[mDxeRuntimePciSegmentLibLastRuntimeRange].PhysicalAddress == (Address & (~(UINTN)EFI_PAGE_MASK))) {
    //
    // Convert the physical address to a virtual address and return the virtual address
    //
    return (Address & EFI_PAGE_MASK) + mDxeRuntimePciSegmentLibRegistrationTable[mDxeRuntimePciSegmentLibLastRuntimeRange].VirtualAddress;
  }

  //
  // Search the entire table for a physical address match
  //
  for (Index = 0; Index < mDxeRuntimePciSegmentLibNumberOfRuntimeRanges; Index++) {
    if (mDxeRuntimePciSegmentLibRegistrationTable[Index].PhysicalAddress == (Address & (~(UINTN)EFI_PAGE_MASK))) {
      //
      // Cache the matching index value
      //
      mDxeRuntimePciSegmentLibLastRuntimeRange = Index;
      //
      // Convert the physical address to a virtual address and return the virtual address
      //
      return (Address & EFI_PAGE_MASK) + mDxeRuntimePciSegmentLibRegistrationTable[Index].VirtualAddress;
    }
  }

  //
  // No match was found.  This is a critical error at OS runtime, so ASSERT() and force a breakpoint.
  //
  ASSERT (FALSE);
  CpuBreakpoint ();

  //
  // Return the physical address
  //
  return Address;
}