/** @file
  Dynamic Platform Info Repository

  Copyright (c) 2021, Arm Limited. All rights reserved.<BR>

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

  @par Glossary:
    - Cm or CM   - Configuration Manager
    - Obj or OBJ - Object
**/

#include <Protocol/ConfigurationManagerProtocol.h>
#include <Library/BaseLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/DebugLib.h>
#include <Library/MemoryAllocationLib.h>

#include "CmObjectTokenFixer.h"
#include "DynamicPlatRepoInternal.h"
#include "TokenGenerator.h"

/** Allocate a CM_OBJ_NODE.

  @param [in]  CmObjDesc  CmObj to wrap in a node.
                          All the fields of the CmObj (Data field included),
                          are copied.
  @param [in]  Token      Token to assign to this CmObj/node.
  @param [out] ObjNode    Allocated ObjNode.

  @retval EFI_SUCCESS           Success.
  @retval EFI_INVALID_PARAMETER A parameter is invalid.
  @retval EFI_OUT_OF_RESOURCES  An allocation has failed.
**/
STATIC
EFI_STATUS
EFIAPI
AllocCmObjNode (
  IN  CONST CM_OBJ_DESCRIPTOR  *CmObjDesc,
  IN        CM_OBJECT_TOKEN    Token,
  OUT       CM_OBJ_NODE        **ObjNode
  )
{
  CM_OBJ_NODE        *Node;
  CM_OBJ_DESCRIPTOR  *Desc;

  if ((CmObjDesc == NULL) || (ObjNode == NULL)) {
    ASSERT (0);
    return EFI_INVALID_PARAMETER;
  }

  Node = AllocateZeroPool (sizeof (CM_OBJ_NODE));
  if (Node == NULL) {
    ASSERT (0);
    return EFI_OUT_OF_RESOURCES;
  }

  // Initialise the list head.
  InitializeListHead (&Node->Link);
  Node->Token    = Token;
  Desc           = &Node->CmObjDesc;
  Desc->ObjectId = CmObjDesc->ObjectId;
  Desc->Size     = CmObjDesc->Size;
  Desc->Count    = CmObjDesc->Count;

  // Allocate and copy the CmObject Data.
  Desc->Data = AllocateCopyPool (CmObjDesc->Size, CmObjDesc->Data);
  if (Desc->Data == NULL) {
    FreePool (Node);
    ASSERT (0);
    return EFI_OUT_OF_RESOURCES;
  }

  *ObjNode = Node;
  return EFI_SUCCESS;
}

/** Free a CM_OBJ_NODE.

  @param [in]  ObjNode    ObjNode to free.

  @retval EFI_SUCCESS           Success.
  @retval EFI_INVALID_PARAMETER A parameter is invalid.
**/
STATIC
EFI_STATUS
EFIAPI
FreeCmObjNode (
  IN  CM_OBJ_NODE  *ObjNode
  )
{
  CM_OBJ_DESCRIPTOR  *Desc;

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

  // Unlink Node
  RemoveEntryList (&ObjNode->Link);

  Desc = &ObjNode->CmObjDesc;
  if (Desc->Data != NULL) {
    FreePool (Desc->Data);
  }

  FreePool (ObjNode);
  return EFI_SUCCESS;
}

/** Add an object to the dynamic platform repository.

  @param [in]  This       This dynamic platform repository.
  @param [in]  CmObjDesc  CmObj to add. The data is copied.
  @param [out] Token      If not NULL, token allocated to this CmObj.

  @retval EFI_SUCCESS           Success.
  @retval EFI_INVALID_PARAMETER A parameter is invalid.
  @retval EFI_OUT_OF_RESOURCES  An allocation has failed.
**/
EFI_STATUS
EFIAPI
DynPlatRepoAddObject (
  IN        DYNAMIC_PLATFORM_REPOSITORY_INFO  *This,
  IN  CONST CM_OBJ_DESCRIPTOR                 *CmObjDesc,
  OUT       CM_OBJECT_TOKEN                   *Token OPTIONAL
  )
{
  EFI_STATUS       Status;
  CM_OBJ_NODE      *ObjNode;
  CM_OBJECT_ID     ArmNamespaceObjId;
  CM_OBJECT_TOKEN  NewToken;

  // The dynamic repository must be able to receive objects.
  if ((This == NULL)      ||
      (CmObjDesc == NULL) ||
      (This->RepoState != DynRepoTransient))
  {
    ASSERT (0);
    return EFI_INVALID_PARAMETER;
  }

  // Check the CmObjDesc:
  //  - only Arm objects are supported for now.
  //  - only EArmObjCmRef objects can be added as arrays.
  ArmNamespaceObjId = GET_CM_OBJECT_ID (CmObjDesc->ObjectId);
  if ((CmObjDesc->Size == 0)              ||
      (CmObjDesc->Count == 0)             ||
      (ArmNamespaceObjId >= EArmObjMax)   ||
      ((CmObjDesc->Count > 1)  && (ArmNamespaceObjId != EArmObjCmRef))  ||
      (GET_CM_NAMESPACE_ID (CmObjDesc->ObjectId) != EObjNameSpaceArm))
  {
    ASSERT (0);
    return EFI_INVALID_PARAMETER;
  }

  // Generate a token.
  NewToken = GenerateToken ();

  // Create an ObjNode.
  Status = AllocCmObjNode (CmObjDesc, NewToken, &ObjNode);
  if (EFI_ERROR (Status)) {
    ASSERT (0);
    return Status;
  }

  // Fixup self-token if necessary.
  Status = FixupCmObjectSelfToken (&ObjNode->CmObjDesc, NewToken);
  if (EFI_ERROR (Status)) {
    FreeCmObjNode (ObjNode);
    ASSERT (0);
    return Status;
  }

  // Add to link list.
  InsertTailList (&This->ArmCmObjList[ArmNamespaceObjId], &ObjNode->Link);
  This->ObjectCount += 1;

  if (Token != NULL) {
    *Token = NewToken;
  }

  return EFI_SUCCESS;
}

/** Group lists of CmObjNode from the ArmNameSpace to one array.

  @param [in]  This         This dynamic platform repository.
  @param [in]  ArmObjIndex  Index in EARM_OBJECT_ID
                            (must be < EArmObjMax).

  @retval EFI_SUCCESS           Success.
  @retval EFI_INVALID_PARAMETER A parameter is invalid.
  @retval EFI_BUFFER_TOO_SMALL    Buffer too small.
  @retval EFI_OUT_OF_RESOURCES  An allocation has failed.
**/
STATIC
EFI_STATUS
EFIAPI
GroupCmObjNodes (
  IN  DYNAMIC_PLATFORM_REPOSITORY_INFO  *This,
  IN  UINT32                            ArmObjIndex
  )
{
  EFI_STATUS         Status;
  UINTN              Count;
  UINTN              Size;
  UINT32             CmObjId;
  UINT8              *GroupedData;
  UINT8              *Data;
  CM_OBJ_DESCRIPTOR  *CmObjDesc;
  LIST_ENTRY         *ListHead;
  LIST_ENTRY         *Link;

  if ((This == NULL)  ||
      (ArmObjIndex >= EArmObjMax))
  {
    ASSERT (0);
    return EFI_INVALID_PARAMETER;
  }

  Count    = 0;
  Size     = 0;
  CmObjId  = CREATE_CM_ARM_OBJECT_ID (ArmObjIndex);
  ListHead = &This->ArmCmObjList[ArmObjIndex];
  Link     = GetFirstNode (ListHead);

  // Compute the total count and size of the CmObj in the list.
  while (Link != ListHead) {
    CmObjDesc = &((CM_OBJ_NODE *)Link)->CmObjDesc;

    if (CmObjDesc->ObjectId != CmObjId) {
      ASSERT (0);
      return EFI_INVALID_PARAMETER;
    }

    if ((CmObjDesc->Count != 1) && (ArmObjIndex != EArmObjCmRef)) {
      // We expect each descriptor to contain an individual object.
      // EArmObjCmRef objects are counted as groups, so +1 as well.
      ASSERT (0);
      return EFI_INVALID_PARAMETER;
    }

    Count++;
    Size += CmObjDesc->Size;

    // Next Link
    Link = GetNextNode (ListHead, Link);
  } // while

  if (Count == 0) {
    // No objects found.
    return EFI_SUCCESS;
  }

  GroupedData = AllocateZeroPool (Size);
  if (GroupedData == NULL) {
    ASSERT (0);
    return EFI_OUT_OF_RESOURCES;
  }

  // Copy the Object Data and add to the TokenMapper.
  Data = GroupedData;
  Link = GetFirstNode (ListHead);
  while (Link != ListHead) {
    CmObjDesc = &((CM_OBJ_NODE *)Link)->CmObjDesc;
    CopyMem (Data, CmObjDesc->Data, CmObjDesc->Size);

    // Add the object to the Token Mapper.
    // Note: The CmObject Data field of objects in the Token Mapper point
    //       to the memory in the GroupedData array.
    Status = TokenMapperAddObject (
               &This->TokenMapper,
               ((CM_OBJ_NODE *)Link)->Token,
               CmObjDesc->ObjectId,
               CmObjDesc->Size,
               Data
               );
    if (EFI_ERROR (Status)) {
      FreePool (GroupedData);
      return Status;
    }

    Data += CmObjDesc->Size;
    Link  = GetNextNode (ListHead, Link);
  } // while

  CmObjDesc           = &This->ArmCmObjArray[ArmObjIndex];
  CmObjDesc->ObjectId = CmObjId;
  CmObjDesc->Size     = Size;
  CmObjDesc->Count    = Count;
  CmObjDesc->Data     = GroupedData;

  return Status;
}

/** Finalise the dynamic repository.

  Finalising means:
   - Preventing any further objects from being added.
   - Allowing to get objects from the dynamic repository
     (not possible before a call to this function).

  @param [in]  This       This dynamic platform repository.

  @retval EFI_SUCCESS           Success.
  @retval EFI_ALREADY_STARTED   Instance already initialised.
  @retval EFI_INVALID_PARAMETER A parameter is invalid.
  @retval EFI_BUFFER_TOO_SMALL  Buffer too small.
  @retval EFI_OUT_OF_RESOURCES  An allocation has failed.
**/
EFI_STATUS
EFIAPI
DynamicPlatRepoFinalise (
  IN  DYNAMIC_PLATFORM_REPOSITORY_INFO  *This
  )
{
  EFI_STATUS  Status;
  UINTN       ArmObjIndex;

  if ((This == NULL)  ||
      (This->RepoState != DynRepoTransient))
  {
    ASSERT (0);
    return EFI_INVALID_PARAMETER;
  }

  // Prevent any further objects from being added.
  This->RepoState = DynRepoFinalized;

  // Initialise the token mapper.
  Status = TokenMapperInitialise (&This->TokenMapper, This->ObjectCount);
  if (EFI_ERROR (Status)) {
    ASSERT (0);
    return Status;
  }

  // For each CM_OBJECT_ID:
  //  - Convert the list of nodes to an array
  //    (the array is wrapped in a CmObjDesc).
  //  - Add the Token/CmObj binding to the token mapper.
  for (ArmObjIndex = 0; ArmObjIndex < EArmObjMax; ArmObjIndex++) {
    Status = GroupCmObjNodes (This, ArmObjIndex);
    if (EFI_ERROR (Status)) {
      ASSERT (0);
      // Free the TokenMapper.
      // Ignore the returned Status since we already failed.
      TokenMapperShutdown (&This->TokenMapper);
      return Status;
    }
  } // for

  return EFI_SUCCESS;
}

/** Get a CmObj from the dynamic repository.

  @param [in]      This        Pointer to the Dynamic Platform Repository.
  @param [in]      CmObjectId  The Configuration Manager Object ID.
  @param [in]      Token       An optional token identifying the object. If
                               unused this must be CM_NULL_TOKEN.
  @param [in, out] CmObjDesc   Pointer to the Configuration Manager Object
                               descriptor describing the requested Object.

  @retval EFI_SUCCESS           Success.
  @retval EFI_INVALID_PARAMETER A parameter is invalid.
  @retval EFI_NOT_FOUND         The required object information is not found.
**/
EFI_STATUS
EFIAPI
DynamicPlatRepoGetObject (
  IN      DYNAMIC_PLATFORM_REPOSITORY_INFO  *This,
  IN      CM_OBJECT_ID                      CmObjectId,
  IN      CM_OBJECT_TOKEN                   Token OPTIONAL,
  IN  OUT CM_OBJ_DESCRIPTOR                 *CmObjDesc
  )
{
  EFI_STATUS         Status;
  CM_OBJ_DESCRIPTOR  *Desc;
  CM_OBJECT_ID       ArmNamespaceObjId;

  if ((This == NULL)      ||
      (CmObjDesc == NULL) ||
      (This->RepoState != DynRepoFinalized))
  {
    ASSERT (0);
    return EFI_INVALID_PARAMETER;
  }

  ArmNamespaceObjId = GET_CM_OBJECT_ID (CmObjectId);
  if (ArmNamespaceObjId >= EArmObjMax) {
    ASSERT (0);
    return EFI_INVALID_PARAMETER;
  }

  if (Token != CM_NULL_TOKEN) {
    // Search in the Token Mapper and return the object.
    Status = TokenMapperGetObject (
               &This->TokenMapper,
               Token,
               CmObjectId,
               CmObjDesc
               );
    ASSERT_EFI_ERROR (Status);
    return Status;
  }

  if (ArmNamespaceObjId == EArmObjCmRef) {
    // EArmObjCmRef object must be requested using a valid token.
    ASSERT (0);
    return EFI_INVALID_PARAMETER;
  }

  Desc = &This->ArmCmObjArray[ArmNamespaceObjId];

  // Nothing here.
  if (Desc->Count == 0) {
    return EFI_NOT_FOUND;
  } else {
    // Return the full array.
    CmObjDesc->ObjectId = Desc->ObjectId;
    CmObjDesc->Size     = Desc->Size;
    CmObjDesc->Data     = Desc->Data;
    CmObjDesc->Count    = Desc->Count;
  }

  return EFI_SUCCESS;
}

/** Initialize the dynamic platform repository.

  @param [out]  DynPlatRepo   If success, contains the initialised dynamic
                              platform repository.

  @retval EFI_SUCCESS           Success.
  @retval EFI_INVALID_PARAMETER A parameter is invalid.
  @retval EFI_OUT_OF_RESOURCES  An allocation has failed.
**/
EFI_STATUS
EFIAPI
DynamicPlatRepoInit (
  OUT DYNAMIC_PLATFORM_REPOSITORY_INFO  **DynPlatRepo
  )
{
  UINTN                             Index;
  DYNAMIC_PLATFORM_REPOSITORY_INFO  *Repo;

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

  Repo = AllocateZeroPool (sizeof (DYNAMIC_PLATFORM_REPOSITORY_INFO));
  if (Repo == NULL) {
    ASSERT (0);
    return EFI_OUT_OF_RESOURCES;
  }

  // Initialise the CmObject List.
  for (Index = 0; Index < EArmObjMax; Index++) {
    InitializeListHead (&Repo->ArmCmObjList[Index]);
  }

  Repo->ObjectCount = 0;
  Repo->RepoState   = DynRepoTransient;

  *DynPlatRepo = Repo;

  return EFI_SUCCESS;
}

/** Shutdown the dynamic platform repository.

  Free all the memory allocated for the dynamic platform repository.

  @param [in]  DynPlatRepo    The dynamic platform repository.

  @retval EFI_INVALID_PARAMETER A parameter is invalid.
  @retval EFI_SUCCESS           Success.
**/
EFI_STATUS
EFIAPI
DynamicPlatRepoShutdown (
  IN  DYNAMIC_PLATFORM_REPOSITORY_INFO  *DynPlatRepo
  )
{
  EFI_STATUS         Status;
  UINT32             Index;
  LIST_ENTRY         *ListHead;
  CM_OBJ_DESCRIPTOR  *CmObjDesc;
  VOID               *Data;

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

  // Free the list of objects.
  for (Index = 0; Index < EArmObjMax; Index++) {
    // Free all the nodes with this object Id.
    ListHead = &DynPlatRepo->ArmCmObjList[Index];
    while (!IsListEmpty (ListHead)) {
      FreeCmObjNode ((CM_OBJ_NODE *)GetFirstNode (ListHead));
    } // while
  } // for

  // Free the arrays.
  CmObjDesc = DynPlatRepo->ArmCmObjArray;
  for (Index = 0; Index < EArmObjMax; Index++) {
    Data = CmObjDesc[Index].Data;
    if (Data != NULL) {
      FreePool (Data);
    }
  } // for

  // Free the TokenMapper
  Status = TokenMapperShutdown (&DynPlatRepo->TokenMapper);
  ASSERT_EFI_ERROR (Status);
  FreePool (DynPlatRepo);
  return Status;
}
