/** @file
File to contain all the hardware specific stuff for the Periodical Timer dispatch protocol.

Copyright (c) 2013-2016 Intel Corporation.

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


**/

//
// Include common header file for this module.
//
#include "CommonHeader.h"

#include "QNCSmmHelpers.h"

typedef enum {
  PERIODIC_TIMER = 0,
  NUM_TIMERS
} SUPPORTED_TIMER;

typedef struct _TIMER_INTERVAL
{
  UINT64    Interval;
  UINT8     AssociatedTimer;
} TIMER_INTERVAL;

//
// Time constants, in 100 nano-second units
//
#define TIME_64s   640000000 /* 64   s  */
#define TIME_32s   320000000 /* 32   s  */
#define TIME_16s   160000000 /* 16   s  */
#define TIME_8s     80000000 /*  8   s  */
#define TIME_64ms     640000 /* 64   ms */
#define TIME_32ms     320000 /* 32   ms */
#define TIME_16ms     160000 /* 16   ms */
#define TIME_1_5ms     15000 /*  1.5 ms */

// PMCW (GPE+28h) [2:0] Periodic SMI Rate selection
// 000 1.5ms
// 001 16ms
// 010 32ms
// 011 64ms
// 100 8s
// 101 16s
// 110 32s
// 111 64s

typedef enum {
  INDEX_TIME_1_5ms = 0,
  INDEX_TIME_16ms,
  INDEX_TIME_32ms,
  INDEX_TIME_64ms,
  INDEX_TIME_8s,
  INDEX_TIME_16s,
  INDEX_TIME_32s,
  INDEX_TIME_64s,
  INDEX_TIME_MAX
} TIMER_INTERVAL_INDEX;

TIMER_INTERVAL mSmmPeriodicTimerIntervals[INDEX_TIME_MAX] = {
  {TIME_1_5ms, PERIODIC_TIMER},
  {TIME_16ms,  PERIODIC_TIMER},
  {TIME_32ms,  PERIODIC_TIMER},
  {TIME_64ms,  PERIODIC_TIMER},
  { TIME_8s,    PERIODIC_TIMER },
  {TIME_16s,   PERIODIC_TIMER},
  {TIME_32s,   PERIODIC_TIMER},
  {TIME_64s,   PERIODIC_TIMER}
};

typedef struct _TIMER_INFO {
  UINTN     NumChildren;      // number of children using this timer
  UINT64    MinReqInterval;   // minimum interval required by children
  UINTN     CurrentSetting;   // interval this timer is set at right now (index into interval table)
} TIMER_INFO;

TIMER_INFO  mTimers[NUM_TIMERS];

QNC_SMM_SOURCE_DESC mTIMER_SOURCE_DESCS[NUM_TIMERS] = {
  {
    QNC_SMM_NO_FLAGS,
    {
      {{GPE_ADDR_TYPE, {R_QNC_GPE0BLK_SMIE}}, S_QNC_GPE0BLK_SMIE, N_QNC_GPE0BLK_SMIE_SWT},
      NULL_BIT_DESC_INITIALIZER
    },
    {
      {{GPE_ADDR_TYPE, {R_QNC_GPE0BLK_SMIS}}, S_QNC_GPE0BLK_SMIS, N_QNC_GPE0BLK_SMIS_SWT}
    }
  }
};

VOID
QNCSmmPeriodicTimerProgramTimers(
  VOID
  );


TIMER_INTERVAL *
ContextToTimerInterval (
  IN  QNC_SMM_CONTEXT     *RegisterContext
  )
{
  UINTN loopvar;

  //
  // Determine which timer this child is using
  //
  for (loopvar = 0; loopvar < INDEX_TIME_MAX; loopvar++) {
    if (((RegisterContext->PeriodicTimer.SmiTickInterval == 0) && (RegisterContext->PeriodicTimer.Period >= mSmmPeriodicTimerIntervals[loopvar].Interval)) ||
        (RegisterContext->PeriodicTimer.SmiTickInterval == mSmmPeriodicTimerIntervals[loopvar].Interval)
       ) {
        return &mSmmPeriodicTimerIntervals[loopvar];
      }
  }

  //
  // If this assertion fires, then either:
  //    (1) the context contains an invalid interval
  //    (2) the timer interval table is corrupt
  //
  // ASSERT (FALSE);

  return NULL;
}

EFI_STATUS
MapPeriodicTimerToSrcDesc (
  IN  QNC_SMM_CONTEXT             *RegisterContext,
  OUT QNC_SMM_SOURCE_DESC         *SrcDesc
  )
{
  TIMER_INTERVAL  *TimerInterval;

  //
  // Figure out which timer the child is requesting and
  // send back the source description
  //
  TimerInterval = ContextToTimerInterval (RegisterContext);
  if (TimerInterval == NULL) {
    return EFI_INVALID_PARAMETER;
  }
  CopyMem (SrcDesc, &mTIMER_SOURCE_DESCS[TimerInterval->AssociatedTimer], sizeof (QNC_SMM_SOURCE_DESC));;

  //
  // Program the value of the interval into hardware
  //
  QNCSmmPeriodicTimerProgramTimers ();

  return EFI_SUCCESS;
}

VOID
PeriodicTimerGetContext (
  IN  DATABASE_RECORD    *Record,
  OUT QNC_SMM_CONTEXT    *HwContext
  )
{
  TIMER_INTERVAL    *TimerInterval;

  ASSERT (Record->ProtocolType == PeriodicTimerType);

  TimerInterval = ContextToTimerInterval (&Record->ChildContext);

  if (TimerInterval != NULL) {
    //
    // Ignore the hardware context. It's not required for this protocol.
    // Instead, just increment the child's context.
    // Update the elapsed time w/ the data from our tables
    //
    Record->CommBuffer.PeriodicTimer.ElapsedTime += TimerInterval->Interval;
    CopyMem (HwContext, &Record->ChildContext, sizeof (QNC_SMM_CONTEXT));
  }
}

BOOLEAN
PeriodicTimerCmpContext (
  IN QNC_SMM_CONTEXT     *HwContext,
  IN QNC_SMM_CONTEXT     *ChildContext
  )
{
  DATABASE_RECORD    *Record;

  Record = DATABASE_RECORD_FROM_CONTEXT (ChildContext);

  if (Record->CommBuffer.PeriodicTimer.ElapsedTime >= ChildContext->PeriodicTimer.Period) {
    //
    // This child should be dispatched
    // The timer will be restarted on the "ClearSource" call.
    //
    return TRUE;
  } else {
    return FALSE;
  }
}

VOID
PeriodicTimerGetBuffer (
  IN  DATABASE_RECORD     * Record
  )
{
  //
  // CommBuffer has been updated by PeriodicTimerGetContext, so return directly
  //
  return;
}

VOID
QNCSmmPeriodicTimerProgramTimers (
  VOID
  )
{
  UINT32            GpePmcwValue;
  SUPPORTED_TIMER   Timer;
  DATABASE_RECORD   *RecordInDb;
  LIST_ENTRY        *LinkInDb;
  TIMER_INTERVAL    *TimerInterval;

  //
  // Find the minimum required interval for each timer
  //
  for (Timer = (SUPPORTED_TIMER)0; Timer < NUM_TIMERS; Timer++) {
    mTimers[Timer].MinReqInterval = ~(UINT64)0x0;
    mTimers[Timer].NumChildren = 0;
  }
  LinkInDb = GetFirstNode (&mPrivateData.CallbackDataBase);
  while (!IsNull (&mPrivateData.CallbackDataBase, LinkInDb)) {
    RecordInDb = DATABASE_RECORD_FROM_LINK (LinkInDb);
    if (RecordInDb->ProtocolType == PeriodicTimerType) {
      //
      // This child is registerd with the PeriodicTimer protocol
      //
      TimerInterval = ContextToTimerInterval (&RecordInDb->ChildContext);

      if(TimerInterval != NULL) {
        Timer = (SUPPORTED_TIMER)((TIMER_INTERVAL *) (TimerInterval))->AssociatedTimer;

        ASSERT (Timer >= 0 && Timer < NUM_TIMERS);

        if (mTimers[Timer].MinReqInterval > RecordInDb->ChildContext.PeriodicTimer.SmiTickInterval) {
          mTimers[Timer].MinReqInterval = RecordInDb->ChildContext.PeriodicTimer.SmiTickInterval;
        }
        mTimers[Timer].NumChildren++;
      }
    }
    LinkInDb = GetNextNode (&mPrivateData.CallbackDataBase, &RecordInDb->Link);
  }

  //
  // Program the hardware
  //
  GpePmcwValue = 0;
  if (mTimers[PERIODIC_TIMER].NumChildren > 0) {
    switch (mTimers[PERIODIC_TIMER].MinReqInterval) {

    case TIME_64s:
      GpePmcwValue = INDEX_TIME_64s;
      mTimers[PERIODIC_TIMER].CurrentSetting = INDEX_TIME_64s;
      break;

    case TIME_32s:
      GpePmcwValue = INDEX_TIME_32s;
      mTimers[PERIODIC_TIMER].CurrentSetting = INDEX_TIME_32s;
      break;

    case TIME_16s:
      GpePmcwValue = INDEX_TIME_16s;
      mTimers[PERIODIC_TIMER].CurrentSetting = INDEX_TIME_16s;
      break;

    case TIME_8s:
      GpePmcwValue = INDEX_TIME_8s;
      mTimers[PERIODIC_TIMER].CurrentSetting = INDEX_TIME_8s;
      break;

    case TIME_64ms:
      GpePmcwValue = INDEX_TIME_64ms;
      mTimers[PERIODIC_TIMER].CurrentSetting = INDEX_TIME_64ms;
      break;

    case TIME_32ms:
      GpePmcwValue = INDEX_TIME_32ms;
      mTimers[PERIODIC_TIMER].CurrentSetting = INDEX_TIME_32ms;
      break;

    case TIME_16ms:
      GpePmcwValue = INDEX_TIME_16ms;
      mTimers[PERIODIC_TIMER].CurrentSetting = INDEX_TIME_16ms;
      break;

    case TIME_1_5ms:
      GpePmcwValue = INDEX_TIME_1_5ms;
      mTimers[PERIODIC_TIMER].CurrentSetting = INDEX_TIME_1_5ms;
      break;

    default:
      ASSERT (FALSE);
      break;
    };

    GpePmcwValue |= B_QNC_GPE0BLK_PMCW_PSE;

    IoOr32(((UINT16)(LpcPciCfg32 (R_QNC_LPC_GPE0BLK) & 0xFFFF) + R_QNC_GPE0BLK_PMCW), GpePmcwValue);

    //
    // Restart the timer here, just need to clear the SMI
    //
    QNCSmmClearSource (&mTIMER_SOURCE_DESCS[PERIODIC_TIMER]);
  } else {
    QNCSmmDisableSource (&mTIMER_SOURCE_DESCS[PERIODIC_TIMER]);
  }
}

EFI_STATUS
QNCSmmPeriodicTimerDispatchGetNextShorterInterval (
  IN CONST EFI_SMM_PERIODIC_TIMER_DISPATCH2_PROTOCOL  *This,
  IN OUT   UINT64                                    **SmiTickInterval
  )
/*++

Routine Description:

  This services returns the next SMI tick period that is supported by the chipset.
  The order returned is from longest to shortest interval period.

Arguments:

  This              - Pointer to the EFI_SMM_PERIODIC_TIMER_DISPATCH2_PROTOCOL instance.
  SmiTickInterval   - Pointer to pointer of the next shorter SMI interval period that is supported by the child.

Returns:

  EFI_SUCCESS              - The service returned successfully.
  EFI_INVALID_PARAMETER   - The parameter SmiTickInterval is invalid.

--*/
{
  TIMER_INTERVAL    *IntervalPointer;

  ASSERT (SmiTickInterval != NULL);

  IntervalPointer = (TIMER_INTERVAL*)*SmiTickInterval;

  if (IntervalPointer == NULL) {
    //
    // The first time child requesting an interval
    //
    IntervalPointer = &mSmmPeriodicTimerIntervals[0];
  } else if (IntervalPointer == &mSmmPeriodicTimerIntervals[INDEX_TIME_MAX - 1]) {
    //
    // At end of the list
    //
    IntervalPointer = NULL;
  } else {
    if ((IntervalPointer >= &mSmmPeriodicTimerIntervals[0]) &&
        (IntervalPointer < &mSmmPeriodicTimerIntervals[INDEX_TIME_MAX - 1])) {
      //
      // Get the next interval in the list
      //
      IntervalPointer++;
    } else {
      //
      // Input is out of range
      //
      return EFI_INVALID_PARAMETER;
    }
  }

  if (IntervalPointer != NULL) {
  *SmiTickInterval = &IntervalPointer->Interval;
  } else {
    *SmiTickInterval = NULL;
  }

  return EFI_SUCCESS;
}

VOID
QNCSmmPeriodicTimerClearSource (
  IN QNC_SMM_SOURCE_DESC     *SrcDesc
  )
/*++

Routine Description:

  This function is responsible for calculating and enabling any timers that are required
  to dispatch messages to children. The SrcDesc argument isn't acutally used.

Arguments:

  SrcDesc - Pointer to the QNC_SMM_SOURCE_DESC instance.

Returns:

  None.

--*/
{
  DATABASE_RECORD   *RecordInDb;
  LIST_ENTRY        *LinkInDb;

  QNCSmmPeriodicTimerProgramTimers ();

  //
  // Reset Elapsed time
  //
  LinkInDb = GetFirstNode (&mPrivateData.CallbackDataBase);
  while (!IsNull (&mPrivateData.CallbackDataBase, LinkInDb)) {
    RecordInDb = DATABASE_RECORD_FROM_LINK (LinkInDb);
    if (RecordInDb->ProtocolType == PeriodicTimerType) {
      //
      // This child is registerd with the PeriodicTimer protocol and Callback
      // has been invoked, so reset the ElapsedTime to 0
      //
      if (RecordInDb->CommBuffer.PeriodicTimer.ElapsedTime >= RecordInDb->ChildContext.PeriodicTimer.Period) {
        RecordInDb->CommBuffer.PeriodicTimer.ElapsedTime = 0;
      }
    }
    LinkInDb = GetNextNode (&mPrivateData.CallbackDataBase, &RecordInDb->Link);
  }
}

