/** @file
  This module produces the SMM Control2 Protocol

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

**/

#include <PiDxe.h>
#include <Protocol/SmmControl2.h>
#include <Library/DebugLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/IoLib.h>
#include <Library/HobLib.h>
#include <Library/UefiRuntimeLib.h>
#include <Library/BaseMemoryLib.h>
#include <Guid/SmmRegisterInfoGuid.h>

#define SMM_DATA_PORT     0xB3
#define SMM_CONTROL_PORT  0xB2

typedef struct {
  UINT8     GblBitOffset;
  UINT8     ApmBitOffset;
  UINT32    Address;
} SMM_CONTROL2_REG;

SMM_CONTROL2_REG  mSmiCtrlReg;

/**
  Invokes SMI activation from either the preboot or runtime environment.

  This function generates an SMI.

  @param[in]     This                The EFI_SMM_CONTROL2_PROTOCOL instance.
  @param[in,out] CommandPort         The value written to the command port.
  @param[in,out] DataPort            The value written to the data port.
  @param[in]     Periodic            Optional mechanism to engender a periodic stream.
  @param[in]     ActivationInterval  Optional parameter to repeat at this period one
                                     time or, if the Periodic Boolean is set, periodically.

  @retval EFI_SUCCESS            The SMI has been engendered.
  @retval EFI_DEVICE_ERROR       The timing is unsupported.
  @retval EFI_INVALID_PARAMETER  The activation period is unsupported.
  @retval EFI_INVALID_PARAMETER  The last periodic activation has not been cleared.
  @retval EFI_NOT_STARTED        The MM base service has not been initialized.
**/
EFI_STATUS
EFIAPI
Activate (
  IN CONST EFI_SMM_CONTROL2_PROTOCOL  *This,
  IN OUT  UINT8                       *CommandPort       OPTIONAL,
  IN OUT  UINT8                       *DataPort          OPTIONAL,
  IN      BOOLEAN                     Periodic           OPTIONAL,
  IN      EFI_SMM_PERIOD              ActivationInterval OPTIONAL
  )
{
  UINT32  SmiEn;
  UINT32  SmiEnableBits;

  if (Periodic) {
    return EFI_INVALID_PARAMETER;
  }

  SmiEn         = IoRead32 (mSmiCtrlReg.Address);
  SmiEnableBits = (1 << mSmiCtrlReg.GblBitOffset) | (1 << mSmiCtrlReg.ApmBitOffset);
  if ((SmiEn & SmiEnableBits) != SmiEnableBits) {
    //
    // Set the "global SMI enable" bit and APM bit
    //
    IoWrite32 (mSmiCtrlReg.Address, SmiEn | SmiEnableBits);
  }

  IoWrite8 (SMM_DATA_PORT, DataPort    == NULL ? 0 : *DataPort);
  IoWrite8 (SMM_CONTROL_PORT, CommandPort == NULL ? 0 : *CommandPort);
  return EFI_SUCCESS;
}

/**
  Clears an SMI.

  @param  This      Pointer to an instance of EFI_SMM_CONTROL2_PROTOCOL
  @param  Periodic  TRUE to indicate a periodical SMI

  @return Return value from SmmClear ()

**/
EFI_STATUS
EFIAPI
Deactivate (
  IN CONST EFI_SMM_CONTROL2_PROTOCOL  *This,
  IN       BOOLEAN                    Periodic
  )
{
  if (Periodic) {
    return EFI_INVALID_PARAMETER;
  }

  //
  // Temporarily do nothing here
  //
  return EFI_SUCCESS;
}

///
/// SMM COntrol2 Protocol instance
///
EFI_SMM_CONTROL2_PROTOCOL  mSmmControl2 = {
  Activate,
  Deactivate,
  0
};

/**
  Get specified SMI register based on given register ID

  @param[in]  SmmRegister  SMI related register array from bootloader
  @param[in]  Id           The register ID to get.

  @retval NULL             The register is not found or the format is not expected.
  @return smi register

**/
PLD_GENERIC_REGISTER *
GetSmmCtrlRegById (
  IN PLD_SMM_REGISTERS  *SmmRegister,
  IN UINT32             Id
  )
{
  UINT32                Index;
  PLD_GENERIC_REGISTER  *PldReg;

  PldReg = NULL;
  for (Index = 0; Index < SmmRegister->Count; Index++) {
    if (SmmRegister->Registers[Index].Id == Id) {
      PldReg = &SmmRegister->Registers[Index];
      break;
    }
  }

  if (PldReg == NULL) {
    DEBUG ((DEBUG_INFO, "Register %d not found.\n", Id));
    return NULL;
  }

  //
  // Checking the register if it is expected.
  //
  if ((PldReg->Address.AccessSize       != EFI_ACPI_3_0_DWORD) ||
      (PldReg->Address.Address          == 0) ||
      (PldReg->Address.RegisterBitWidth != 1) ||
      (PldReg->Address.AddressSpaceId   != EFI_ACPI_3_0_SYSTEM_IO) ||
      (PldReg->Value != 1))
  {
    DEBUG ((DEBUG_INFO, "Unexpected SMM register.\n"));
    DEBUG ((DEBUG_INFO, "AddressSpaceId= 0x%x\n", PldReg->Address.AddressSpaceId));
    DEBUG ((DEBUG_INFO, "RegBitWidth   = 0x%x\n", PldReg->Address.RegisterBitWidth));
    DEBUG ((DEBUG_INFO, "RegBitOffset  = 0x%x\n", PldReg->Address.RegisterBitOffset));
    DEBUG ((DEBUG_INFO, "AccessSize    = 0x%x\n", PldReg->Address.AccessSize));
    DEBUG ((DEBUG_INFO, "Address       = 0x%lx\n", PldReg->Address.Address));
    return NULL;
  }

  return PldReg;
}

/**
  Fixup data pointers so that the services can be called in virtual mode.

  @param[in] Event                The event registered.
  @param[in] Context              Event context.

**/
VOID
EFIAPI
SmmControlVirtualAddressChangeEvent (
  IN EFI_EVENT  Event,
  IN VOID       *Context
  )
{
  EfiConvertPointer (0x0, (VOID **)&(mSmmControl2.Trigger));
  EfiConvertPointer (0x0, (VOID **)&(mSmmControl2.Clear));
}

/**
  This function installs EFI_SMM_CONTROL2_PROTOCOL.

  @param  ImageHandle Handle for the image of this driver
  @param  SystemTable Pointer to the EFI System Table

  @retval EFI_UNSUPPORTED There's no Intel ICH on this platform
  @return The status returned from InstallProtocolInterface().

**/
EFI_STATUS
EFIAPI
SmmControlEntryPoint (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS            Status;
  EFI_HOB_GUID_TYPE     *GuidHob;
  PLD_SMM_REGISTERS     *SmmRegister;
  PLD_GENERIC_REGISTER  *SmiGblEnReg;
  PLD_GENERIC_REGISTER  *SmiApmEnReg;
  EFI_EVENT             Event;

  GuidHob = GetFirstGuidHob (&gSmmRegisterInfoGuid);
  if (GuidHob == NULL) {
    return EFI_UNSUPPORTED;
  }

  SmmRegister = (PLD_SMM_REGISTERS *)(GET_GUID_HOB_DATA (GuidHob));
  SmiGblEnReg = GetSmmCtrlRegById (SmmRegister, REGISTER_ID_SMI_GBL_EN);
  if (SmiGblEnReg == NULL) {
    DEBUG ((DEBUG_ERROR, "SMI global enable reg not found.\n"));
    return EFI_NOT_FOUND;
  }

  mSmiCtrlReg.Address      = (UINT32)SmiGblEnReg->Address.Address;
  mSmiCtrlReg.GblBitOffset = SmiGblEnReg->Address.RegisterBitOffset;

  SmiApmEnReg = GetSmmCtrlRegById (SmmRegister, REGISTER_ID_SMI_APM_EN);
  if (SmiApmEnReg == NULL) {
    DEBUG ((DEBUG_ERROR, "SMI APM enable reg not found.\n"));
    return EFI_NOT_FOUND;
  }

  if (SmiApmEnReg->Address.Address != mSmiCtrlReg.Address) {
    DEBUG ((DEBUG_ERROR, "SMI APM EN and SMI GBL EN are expected to have same register base\n"));
    DEBUG ((DEBUG_ERROR, "APM:0x%x, GBL:0x%x\n", SmiApmEnReg->Address.Address, mSmiCtrlReg.Address));
    return EFI_UNSUPPORTED;
  }

  mSmiCtrlReg.ApmBitOffset = SmiApmEnReg->Address.RegisterBitOffset;

  //
  // Install our protocol interfaces on the device's handle
  //
  Status = gBS->InstallMultipleProtocolInterfaces (
                  &ImageHandle,
                  &gEfiSmmControl2ProtocolGuid,
                  &mSmmControl2,
                  NULL
                  );
  ASSERT_EFI_ERROR (Status);

  Status = gBS->CreateEventEx (
                  EVT_NOTIFY_SIGNAL,
                  TPL_NOTIFY,
                  SmmControlVirtualAddressChangeEvent,
                  NULL,
                  &gEfiEventVirtualAddressChangeGuid,
                  &Event
                  );
  return Status;
}
