/** @file

  Copyright (c) 2017-2021, Arm Limited. All rights reserved.

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

  System Control and Management Interface V1.0
    http://infocenter.arm.com/help/topic/com.arm.doc.den0056a/
    DEN0056A_System_Control_and_Management_Interface.pdf
**/

#include <Library/BaseLib.h>
#include <Library/DebugLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Protocol/ArmScmiClockProtocol.h>
#include <Protocol/ArmScmiClock2Protocol.h>

#include "ArmScmiClockProtocolPrivate.h"
#include "ScmiPrivate.h"

/** Convert to 64 bit value from two 32 bit words.

  @param[in] Low   Lower 32 bits.
  @param[in] High  Higher 32 bits.

  @retval UINT64   64 bit value.
**/
STATIC
UINT64
ConvertTo64Bit (
  IN UINT32  Low,
  IN UINT32  High
  )
{
  return (Low | ((UINT64)High << 32));
}

/** Return version of the clock management protocol supported by SCP firmware.

  @param[in]  This     A Pointer to SCMI_CLOCK_PROTOCOL Instance.

  @param[out] Version  Version of the supported SCMI Clock management protocol.

  @retval EFI_SUCCESS       The version is returned.
  @retval EFI_DEVICE_ERROR  SCP returns an SCMI error.
  @retval !(EFI_SUCCESS)    Other errors.
**/
STATIC
EFI_STATUS
ClockGetVersion (
  IN  SCMI_CLOCK_PROTOCOL  *This,
  OUT UINT32               *Version
  )
{
  return ScmiGetProtocolVersion (ScmiProtocolIdClock, Version);
}

/** Return total number of clock devices supported by the clock management
  protocol.

  @param[in]  This         A Pointer to SCMI_CLOCK_PROTOCOL Instance.

  @param[out] TotalClocks  Total number of clocks supported.

  @retval EFI_SUCCESS       Total number of clocks supported is returned.
  @retval EFI_DEVICE_ERROR  SCP returns an SCMI error.
  @retval !(EFI_SUCCESS)    Other errors.
**/
STATIC
EFI_STATUS
ClockGetTotalClocks (
  IN  SCMI_CLOCK_PROTOCOL  *This,
  OUT UINT32               *TotalClocks
  )
{
  EFI_STATUS  Status;
  UINT32      *ReturnValues;

  Status = ScmiGetProtocolAttributes (ScmiProtocolIdClock, &ReturnValues);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  *TotalClocks = SCMI_CLOCK_PROTOCOL_TOTAL_CLKS (ReturnValues[0]);

  return EFI_SUCCESS;
}

/** Return attributes of a clock device.

  @param[in]  This        A Pointer to SCMI_CLOCK_PROTOCOL Instance.
  @param[in]  ClockId     Identifier for the clock device.

  @param[out] Enabled         If TRUE, the clock device is enabled.
  @param[out] ClockAsciiName  A NULL terminated ASCII string with the clock
                              name, of up to 16 bytes.

  @retval EFI_SUCCESS          Clock device attributes are returned.
  @retval EFI_DEVICE_ERROR     SCP returns an SCMI error.
  @retval !(EFI_SUCCESS)       Other errors.
**/
STATIC
EFI_STATUS
ClockGetClockAttributes (
  IN  SCMI_CLOCK_PROTOCOL  *This,
  IN  UINT32               ClockId,
  OUT BOOLEAN              *Enabled,
  OUT CHAR8                *ClockAsciiName
  )
{
  EFI_STATUS  Status;

  UINT32            *MessageParams;
  CLOCK_ATTRIBUTES  *ClockAttributes;
  SCMI_COMMAND      Cmd;
  UINT32            PayloadLength;

  Status = ScmiCommandGetPayload (&MessageParams);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  *MessageParams = ClockId;

  Cmd.ProtocolId = ScmiProtocolIdClock;
  Cmd.MessageId  = ScmiMessageIdClockAttributes;

  PayloadLength = sizeof (ClockId);

  Status = ScmiCommandExecute (
             &Cmd,
             &PayloadLength,
             (UINT32 **)&ClockAttributes
             );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  // TRUE if bit 0 of ClockAttributes->Attributes is set.
  *Enabled = CLOCK_ENABLED (ClockAttributes->Attributes);

  AsciiStrCpyS (
    ClockAsciiName,
    SCMI_MAX_STR_LEN,
    (CONST CHAR8 *)ClockAttributes->ClockName
    );

  return EFI_SUCCESS;
}

/** Return list of rates supported by a given clock device.

  @param[in] This        A pointer to SCMI_CLOCK_PROTOCOL Instance.
  @param[in] ClockId     Identifier for the clock device.

  @param[out] Format      ScmiClockRateFormatDiscrete: Clock device
                          supports range of clock rates which are non-linear.

                          ScmiClockRateFormatLinear: Clock device supports
                          range of linear clock rates from Min to Max in steps.

  @param[out] TotalRates  Total number of rates.

  @param[in,out] RateArraySize  Size of the RateArray.

  @param[out] RateArray   List of clock rates.

  @retval EFI_SUCCESS          List of clock rates is returned.
  @retval EFI_DEVICE_ERROR     SCP returns an SCMI error.
  @retval EFI_BUFFER_TOO_SMALL RateArraySize is too small for the result.
                               It has been updated to the size needed.
  @retval !(EFI_SUCCESS)       Other errors.
**/
STATIC
EFI_STATUS
ClockDescribeRates (
  IN     SCMI_CLOCK_PROTOCOL     *This,
  IN     UINT32                  ClockId,
  OUT    SCMI_CLOCK_RATE_FORMAT  *Format,
  OUT    UINT32                  *TotalRates,
  IN OUT UINT32                  *RateArraySize,
  OUT    SCMI_CLOCK_RATE         *RateArray
  )
{
  EFI_STATUS  Status;

  UINT32                PayloadLength;
  SCMI_COMMAND          Cmd;
  UINT32                *MessageParams;
  CLOCK_DESCRIBE_RATES  *DescribeRates;
  CLOCK_RATE_DWORD      *Rate;

  UINT32  RequiredArraySize;
  UINT32  RateIndex;
  UINT32  RateNo;
  UINT32  RateOffset;

  *TotalRates       = 0;
  RequiredArraySize = 0;
  RateIndex         = 0;

  Status = ScmiCommandGetPayload (&MessageParams);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  Cmd.ProtocolId = ScmiProtocolIdClock;
  Cmd.MessageId  = ScmiMessageIdClockDescribeRates;

  *MessageParams++ = ClockId;

  do {
    *MessageParams = RateIndex;

    // Set Payload length, note PayloadLength is a IN/OUT parameter.
    PayloadLength = sizeof (ClockId) + sizeof (RateIndex);

    // Execute and wait for response on a SCMI channel.
    Status = ScmiCommandExecute (
               &Cmd,
               &PayloadLength,
               (UINT32 **)&DescribeRates
               );
    if (EFI_ERROR (Status)) {
      return Status;
    }

    if (*TotalRates == 0) {
      // In the first iteration we will get number of returned rates and number
      // of remaining rates. With this information calculate required size
      // for rate array. If provided RateArraySize is less, return an
      // error.

      *Format = RATE_FORMAT (DescribeRates->NumRatesFlags);

      *TotalRates = NUM_RATES (DescribeRates->NumRatesFlags)
                    + NUM_REMAIN_RATES (DescribeRates->NumRatesFlags);

      RequiredArraySize = (*TotalRates) * sizeof (UINT64);

      if (RequiredArraySize > (*RateArraySize)) {
        *RateArraySize = RequiredArraySize;
        return EFI_BUFFER_TOO_SMALL;
      }
    }

    RateOffset = 0;

    if (*Format == ScmiClockRateFormatDiscrete) {
      for (RateNo = 0; RateNo < NUM_RATES (DescribeRates->NumRatesFlags); RateNo++) {
        Rate = &DescribeRates->Rates[RateOffset++];
        // Non-linear discrete rates.
        RateArray[RateIndex++].DiscreteRate.Rate =
          ConvertTo64Bit (Rate->Low, Rate->High);
      }
    } else {
      // Linear clock rates from minimum to maximum in steps
      // Minimum clock rate.
      Rate                                    = &DescribeRates->Rates[RateOffset++];
      RateArray[RateIndex].ContinuousRate.Min =
        ConvertTo64Bit (Rate->Low, Rate->High);

      Rate = &DescribeRates->Rates[RateOffset++];
      // Maximum clock rate.
      RateArray[RateIndex].ContinuousRate.Max =
        ConvertTo64Bit (Rate->Low, Rate->High);

      Rate = &DescribeRates->Rates[RateOffset++];
      // Step.
      RateArray[RateIndex++].ContinuousRate.Step =
        ConvertTo64Bit (Rate->Low, Rate->High);
    }
  } while (NUM_REMAIN_RATES (DescribeRates->NumRatesFlags) != 0);

  // Update RateArraySize with RequiredArraySize.
  *RateArraySize = RequiredArraySize;

  return EFI_SUCCESS;
}

/** Get clock rate.

  @param[in]  This        A Pointer to SCMI_CLOCK_PROTOCOL Instance.
  @param[in]  ClockId     Identifier for the clock device.

  @param[out]  Rate       Clock rate.

  @retval EFI_SUCCESS          Clock rate is returned.
  @retval EFI_DEVICE_ERROR     SCP returns an SCMI error.
  @retval !(EFI_SUCCESS)       Other errors.
**/
STATIC
EFI_STATUS
ClockRateGet (
  IN  SCMI_CLOCK_PROTOCOL  *This,
  IN  UINT32               ClockId,
  OUT UINT64               *Rate
  )
{
  EFI_STATUS  Status;

  UINT32            *MessageParams;
  CLOCK_RATE_DWORD  *ClockRate;
  SCMI_COMMAND      Cmd;

  UINT32  PayloadLength;

  Status = ScmiCommandGetPayload (&MessageParams);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  // Fill arguments for clock protocol command.
  *MessageParams = ClockId;

  Cmd.ProtocolId = ScmiProtocolIdClock;
  Cmd.MessageId  = ScmiMessageIdClockRateGet;

  PayloadLength = sizeof (ClockId);

  // Execute and wait for response on a SCMI channel.
  Status = ScmiCommandExecute (
             &Cmd,
             &PayloadLength,
             (UINT32 **)&ClockRate
             );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  *Rate = ConvertTo64Bit (ClockRate->Low, ClockRate->High);

  return EFI_SUCCESS;
}

/** Set clock rate.

  @param[in]  This        A Pointer to SCMI_CLOCK_PROTOCOL Instance.
  @param[in]  ClockId     Identifier for the clock device.
  @param[in]  Rate        Clock rate.

  @retval EFI_SUCCESS          Clock rate set success.
  @retval EFI_DEVICE_ERROR     SCP returns an SCMI error.
  @retval !(EFI_SUCCESS)       Other errors.
**/
STATIC
EFI_STATUS
ClockRateSet (
  IN SCMI_CLOCK_PROTOCOL  *This,
  IN UINT32               ClockId,
  IN UINT64               Rate
  )
{
  EFI_STATUS                 Status;
  CLOCK_RATE_SET_ATTRIBUTES  *ClockRateSetAttributes;
  SCMI_COMMAND               Cmd;
  UINT32                     PayloadLength;

  Status = ScmiCommandGetPayload ((UINT32 **)&ClockRateSetAttributes);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  // Fill arguments for clock protocol command.
  ClockRateSetAttributes->ClockId   = ClockId;
  ClockRateSetAttributes->Flags     = CLOCK_SET_DEFAULT_FLAGS;
  ClockRateSetAttributes->Rate.Low  = (UINT32)Rate;
  ClockRateSetAttributes->Rate.High = (UINT32)(Rate >> 32);

  Cmd.ProtocolId = ScmiProtocolIdClock;
  Cmd.MessageId  = ScmiMessageIdClockRateSet;

  PayloadLength = sizeof (CLOCK_RATE_SET_ATTRIBUTES);

  // Execute and wait for response on a SCMI channel.
  Status = ScmiCommandExecute (
             &Cmd,
             &PayloadLength,
             NULL
             );

  return Status;
}

/** Enable/Disable specified clock.

  @param[in]  This        A Pointer to SCMI_CLOCK_PROTOCOL Instance.
  @param[in]  ClockId     Identifier for the clock device.
  @param[in]  Enable      TRUE to enable, FALSE to disable.

  @retval EFI_SUCCESS          Clock enable/disable successful.
  @retval EFI_DEVICE_ERROR     SCP returns an SCMI error.
  @retval !(EFI_SUCCESS)       Other errors.
**/
STATIC
EFI_STATUS
ClockEnable (
  IN SCMI_CLOCK2_PROTOCOL  *This,
  IN UINT32                ClockId,
  IN BOOLEAN               Enable
  )
{
  EFI_STATUS                   Status;
  CLOCK_CONFIG_SET_ATTRIBUTES  *ClockConfigSetAttributes;
  SCMI_COMMAND                 Cmd;
  UINT32                       PayloadLength;

  Status = ScmiCommandGetPayload ((UINT32 **)&ClockConfigSetAttributes);
  if (EFI_ERROR (Status)) {
    return Status;
  }

  // Fill arguments for clock protocol command.
  ClockConfigSetAttributes->ClockId    = ClockId;
  ClockConfigSetAttributes->Attributes = Enable ? BIT0 : 0;

  Cmd.ProtocolId = ScmiProtocolIdClock;
  Cmd.MessageId  = ScmiMessageIdClockConfigSet;

  PayloadLength = sizeof (CLOCK_CONFIG_SET_ATTRIBUTES);

  // Execute and wait for response on a SCMI channel.
  Status = ScmiCommandExecute (
             &Cmd,
             &PayloadLength,
             NULL
             );

  return Status;
}

// Instance of the SCMI clock management protocol.
STATIC CONST SCMI_CLOCK_PROTOCOL  ScmiClockProtocol = {
  ClockGetVersion,
  ClockGetTotalClocks,
  ClockGetClockAttributes,
  ClockDescribeRates,
  ClockRateGet,
  ClockRateSet
};

// Instance of the SCMI clock management protocol.
STATIC CONST SCMI_CLOCK2_PROTOCOL  ScmiClock2Protocol = {
  (SCMI_CLOCK2_GET_VERSION)ClockGetVersion,
  (SCMI_CLOCK2_GET_TOTAL_CLOCKS)ClockGetTotalClocks,
  (SCMI_CLOCK2_GET_CLOCK_ATTRIBUTES)ClockGetClockAttributes,
  (SCMI_CLOCK2_DESCRIBE_RATES)ClockDescribeRates,
  (SCMI_CLOCK2_RATE_GET)ClockRateGet,
  (SCMI_CLOCK2_RATE_SET)ClockRateSet,
  SCMI_CLOCK2_PROTOCOL_VERSION,
  ClockEnable
};

/** Initialize clock management protocol and install protocol on a given handle.

  @param[in] Handle              Handle to install clock management protocol.

  @retval EFI_SUCCESS            Clock protocol interface installed successfully.
**/
EFI_STATUS
ScmiClockProtocolInit (
  IN EFI_HANDLE  *Handle
  )
{
  return gBS->InstallMultipleProtocolInterfaces (
                Handle,
                &gArmScmiClockProtocolGuid,
                &ScmiClockProtocol,
                &gArmScmiClock2ProtocolGuid,
                &ScmiClock2Protocol,
                NULL
                );
}
