/** @file
  FDT client library for consumers of PCI related dynamic PCDs

  Copyright (c) 2016, Linaro Ltd. All rights reserved.<BR>

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

**/

#include <Uefi.h>

#include <Library/BaseLib.h>
#include <Library/DebugLib.h>
#include <Library/PcdLib.h>
#include <Library/UefiBootServicesTableLib.h>

#include <Protocol/FdtClient.h>

//
// We expect the "ranges" property of "pci-host-ecam-generic" to consist of
// records like this.
//
#pragma pack (1)
typedef struct {
  UINT32    Type;
  UINT64    ChildBase;
  UINT64    CpuBase;
  UINT64    Size;
} DTB_PCI_HOST_RANGE_RECORD;
#pragma pack ()

#define DTB_PCI_HOST_RANGE_RELOCATABLE   BIT31
#define DTB_PCI_HOST_RANGE_PREFETCHABLE  BIT30
#define DTB_PCI_HOST_RANGE_ALIASED       BIT29
#define DTB_PCI_HOST_RANGE_MMIO32        BIT25
#define DTB_PCI_HOST_RANGE_MMIO64        (BIT25 | BIT24)
#define DTB_PCI_HOST_RANGE_IO            BIT24
#define DTB_PCI_HOST_RANGE_TYPEMASK      (BIT31 | BIT30 | BIT29 | BIT25 | BIT24)

STATIC
RETURN_STATUS
GetPciIoTranslation (
  IN  FDT_CLIENT_PROTOCOL  *FdtClient,
  IN  INT32                Node,
  OUT UINT64               *IoTranslation
  )
{
  UINT32      RecordIdx;
  CONST VOID  *Prop;
  UINT32      Len;
  EFI_STATUS  Status;
  UINT64      IoBase;

  //
  // Iterate over "ranges".
  //
  Status = FdtClient->GetNodeProperty (FdtClient, Node, "ranges", &Prop, &Len);
  if (EFI_ERROR (Status) || (Len == 0) ||
      (Len % sizeof (DTB_PCI_HOST_RANGE_RECORD) != 0))
  {
    DEBUG ((DEBUG_ERROR, "%a: 'ranges' not found or invalid\n", __FUNCTION__));
    return RETURN_PROTOCOL_ERROR;
  }

  for (RecordIdx = 0; RecordIdx < Len / sizeof (DTB_PCI_HOST_RANGE_RECORD);
       ++RecordIdx)
  {
    CONST DTB_PCI_HOST_RANGE_RECORD  *Record;
    UINT32                           Type;

    Record = (CONST DTB_PCI_HOST_RANGE_RECORD *)Prop + RecordIdx;
    Type   = SwapBytes32 (Record->Type) & DTB_PCI_HOST_RANGE_TYPEMASK;
    if (Type == DTB_PCI_HOST_RANGE_IO) {
      IoBase         = SwapBytes64 (Record->ChildBase);
      *IoTranslation = SwapBytes64 (Record->CpuBase) - IoBase;

      return RETURN_SUCCESS;
    }
  }

  return RETURN_NOT_FOUND;
}

RETURN_STATUS
EFIAPI
FdtPciPcdProducerLibConstructor (
  VOID
  )
{
  UINT64               PciExpressBaseAddress;
  FDT_CLIENT_PROTOCOL  *FdtClient;
  CONST UINT64         *Reg;
  UINT32               RegSize;
  EFI_STATUS           Status;
  INT32                Node;
  RETURN_STATUS        RetStatus;
  UINT64               IoTranslation;
  RETURN_STATUS        PcdStatus;

  PciExpressBaseAddress = PcdGet64 (PcdPciExpressBaseAddress);
  if (PciExpressBaseAddress != MAX_UINT64) {
    //
    // Assume that the fact that PciExpressBaseAddress has been changed from
    // its default value of MAX_UINT64 implies that this code has been
    // executed already, in the context of another module. That means we can
    // assume that PcdPciIoTranslation has been discovered from the DT node
    // as well.
    //
    return EFI_SUCCESS;
  }

  Status = gBS->LocateProtocol (
                  &gFdtClientProtocolGuid,
                  NULL,
                  (VOID **)&FdtClient
                  );
  ASSERT_EFI_ERROR (Status);

  PciExpressBaseAddress = 0;
  Status                = FdtClient->FindCompatibleNode (
                                       FdtClient,
                                       "pci-host-ecam-generic",
                                       &Node
                                       );

  if (!EFI_ERROR (Status)) {
    Status = FdtClient->GetNodeProperty (
                          FdtClient,
                          Node,
                          "reg",
                          (CONST VOID **)&Reg,
                          &RegSize
                          );

    if (!EFI_ERROR (Status) && (RegSize == 2 * sizeof (UINT64))) {
      PciExpressBaseAddress = SwapBytes64 (*Reg);

      PcdStatus = PcdSetBoolS (PcdPciDisableBusEnumeration, FALSE);
      ASSERT_RETURN_ERROR (PcdStatus);

      IoTranslation = 0;
      RetStatus     = GetPciIoTranslation (FdtClient, Node, &IoTranslation);
      if (!RETURN_ERROR (RetStatus)) {
        PcdStatus = PcdSet64S (PcdPciIoTranslation, IoTranslation);
        ASSERT_RETURN_ERROR (PcdStatus);
      } else {
        //
        // Support for I/O BARs is not mandatory, and so it does not make sense
        // to abort in the general case. So leave it up to the actual driver to
        // complain about this if it wants to, and just issue a warning here.
        //
        DEBUG ((
          DEBUG_WARN,
          "%a: 'pci-host-ecam-generic' device encountered with no I/O range\n",
          __FUNCTION__
          ));
      }
    }
  }

  PcdStatus = PcdSet64S (PcdPciExpressBaseAddress, PciExpressBaseAddress);
  ASSERT_RETURN_ERROR (PcdStatus);

  return RETURN_SUCCESS;
}
