/* Copyright (c) 2009-2019 Arm Limited
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*************************************************************************************************/
/*!
 *  \brief SMP device database.
 */
/*************************************************************************************************/

#include <string.h>
#include "wsf_types.h"
#include "wsf_trace.h"
#include "wsf_assert.h"
#include "wsf_timer.h"
#include "smp_api.h"
#include "smp_main.h"

/**************************************************************************************************
  Macros
**************************************************************************************************/

/* Period in ms to service the database. */
#define SMP_DB_SRV_MS               1000

/* Decrement a timer. */
#define SMP_DB_DEC_TIMER(a)         do { a = a > SMP_DB_SRV_MS ? a - SMP_DB_SRV_MS : 0;} while (0)

/* Device database indicies. */
#define SMP_DB_COMMON_REC           0     /*! Common record used when database is full. */
#define SMP_DB_FIRST_REC            1     /*! Index of first device specific record in database. */

/**************************************************************************************************
  Data Types
**************************************************************************************************/

typedef struct
{
  bdAddr_t      peerAddr;                 /*! Peer address. */
  uint8_t       addrType;                 /*! Peer address type. */
  uint8_t       failCount;                /*! Prior attempt failure count. */
  uint16_t      attemptMult;              /*! Attempts timeout multiplier (0 - record not used). */
  uint32_t      lockMs;                   /*! Time remaining until device can attempt pairing. */
  uint32_t      expDecrementMs;           /*! Time remaining until attempt attemptMult decreases. */
  uint32_t      failCountToMs;            /*! Time remaining until failCount is cleared. */
} smpDbDevice_t;

/**************************************************************************************************
  Local Variables
**************************************************************************************************/

/* Control block. */
static struct
{
  smpDbDevice_t db[SMP_DB_MAX_DEVICES];   /*! Device database. */
  wsfTimer_t    serviceTimer;             /*! Timer to service database. */
} smpDbCb;

/*************************************************************************************************/
/*!
 *  \brief  Ensure the SMP DB service timer is running.
 *
 *  \return None.
 */
/*************************************************************************************************/
static void smpDbStartServiceTimer(void)
{
  if (smpDbCb.serviceTimer.isStarted == FALSE)
  {
    WsfTimerStartMs(&smpDbCb.serviceTimer, SMP_DB_SRV_MS);
  }
}

/*************************************************************************************************/
/*!
 *  \brief  Check if a database record is in use.
 *
 *  \param  pRec    Pointer to the record.
 *
 *  \return TRUE if record in use, else FALSE.
 */
/*************************************************************************************************/
static bool_t smpDbRecordInUse(smpDbDevice_t *pRec)
{
  /* When failCount, lockMs, and attemptMult are zero, the record can be used for another device. */
  if (pRec->failCount > 0)
  {
    return TRUE;
  }

  if (pRec->lockMs > 0)
  {
    return TRUE;
  }

  if (pRec->attemptMult > 0)
  {
    return TRUE;
  }

  return FALSE;
}

/*************************************************************************************************/
/*!
 *  \brief  Add a device to the database.
 *
 *  \param  pAddr     Peer BD address.
 *  \param  addrType  Peer BD address type.
 *
 *  \return Pointer to DB record or NULL if database full.
 */
/*************************************************************************************************/
static smpDbDevice_t *smpDbAddDevice(uint8_t *pAddr, uint8_t addrType)
{
  smpDbDevice_t *pRec = &smpDbCb.db[SMP_DB_FIRST_REC];
  uint8_t i;

  SMP_TRACE_INFO0("smpDbAddDevice");

  for (i = SMP_DB_FIRST_REC; i < SMP_DB_MAX_DEVICES; i++, pRec++)
  {
    if (smpDbRecordInUse(pRec) == FALSE)
    {
      /* Reset record. */
      memset(pRec, 0, sizeof(smpDbDevice_t));

      pRec->addrType = addrType;
      BdaCpy(pRec->peerAddr, pAddr);
      return pRec;
    }
  }

  return NULL;
}

/*************************************************************************************************/
/*!
 *  \brief  Get a record in the database.
 *
 *  \param  connId    Connection identifier.
 *
 *  \return Pointer to the record associated with the connection or the common record.
 */
/*************************************************************************************************/
static smpDbDevice_t *smpDbGetRecord(dmConnId_t connId)
{
  smpDbDevice_t *pRec = &smpDbCb.db[SMP_DB_FIRST_REC];
  uint8_t addrType = DmHostAddrType(DmConnPeerAddrType(connId));
  uint8_t *pAddr = DmConnPeerAddr(connId);
  uint8_t i;

  SMP_TRACE_INFO2("smpDbGetRecord: connId: %d type: %d", connId, addrType);

  for (i = SMP_DB_FIRST_REC; i < SMP_DB_MAX_DEVICES; i++, pRec++)
  {
    if (smpDbRecordInUse(pRec) && (pRec->addrType == addrType) && BdaCmp(pRec->peerAddr, pAddr))
    {
      return pRec;
    }
  }

  /* Device is not in the database, add the device. */
  pRec = smpDbAddDevice(pAddr, addrType);

  if (pRec == NULL)
  {
    SMP_TRACE_INFO0("smpDbGetRecord: common record");

    /* Database is full, use the common record. */
    pRec = &smpDbCb.db[SMP_DB_COMMON_REC];
  }

  return pRec;
}

/*************************************************************************************************/
/*!
 *  \brief  Initialize the SMP Database.
 *
 *  \return None.
 */
/*************************************************************************************************/
void SmpDbInit(void)
{
  /* Stop active service timer. */
  if (smpDbCb.serviceTimer.isStarted == TRUE)
  {
    WsfTimerStop(&smpDbCb.serviceTimer);
  }

  /* Reset control block. */
  memset(&smpDbCb, 0, sizeof(smpDbCb));

  /* Setup service timer. */
  smpDbCb.serviceTimer.handlerId = smpCb.handlerId;
  smpDbCb.serviceTimer.msg.event = SMP_DB_SERVICE_IND;
}

/*************************************************************************************************/
/*!
 *  \brief  Get the time (msec) that pairing is disabled for a device.
 *
 *  \param  connId    Connection identifier.
 *
 *  \return Time pairing is disabled (msec), or zero if pairing isn't disabled.
 */
/*************************************************************************************************/
uint32_t SmpDbGetPairingDisabledTime(dmConnId_t connId)
{
  smpDbDevice_t *pRec = smpDbGetRecord(connId);

  SMP_TRACE_INFO3("SmpDbGetPairingDisabledTime: connId: %d period: %d attemptMult: %d",
                  connId, pRec->lockMs, pRec->attemptMult);

  return pRec->lockMs;
}

/*************************************************************************************************/
/*!
 *  \brief  Set the count of prior failures for a device.
 *
 *  \param  connId    Connection identifier.
 *  \param  count     Failure count.
 *
 *  \return None.
 */
/*************************************************************************************************/
void SmpDbSetFailureCount(dmConnId_t connId, uint8_t count)
{
  smpDbDevice_t *pRec = smpDbGetRecord(connId);

  SMP_TRACE_INFO2("SmpDbSetFailureCount: connId: %d count: %d", connId, count);

  pRec->failCount = count;

  if (count != 0)
  {
    pRec->failCountToMs = pSmpCfg->maxAttemptTimeout;
  }
}

/*************************************************************************************************/
/*!
 *  \brief  Get the count of prior failures for a device.
 *
 *  \param  connId    Connection identifier.
 *
 *  \return The failure count.
 */
/*************************************************************************************************/
uint8_t SmpDbGetFailureCount(dmConnId_t connId)
{
  smpDbDevice_t *pRec = smpDbGetRecord(connId);

  SMP_TRACE_INFO2("SmpDbGetFailureCount: connId: %d count: %d", connId, pRec->failCount);

  return pRec->failCount;
}

/*************************************************************************************************/
/*!
 *  \brief  Called to report max pairing attempts was reached for a device.
 *
 *  \param  connId    Connection identifier.
 *
 *  \return Time until device can retry pairing.
 */
/*************************************************************************************************/
uint32_t SmpDbMaxAttemptReached(dmConnId_t connId)
{
  smpDbDevice_t *pRec = smpDbGetRecord(connId);
  uint16_t multiplier;

  SMP_TRACE_INFO1("SmpDbMaxAttemptReached: connId: %d", connId);

  if (pRec->attemptMult == 0)
  {
    /* Due to a disconnection, a record exists but the attempt multipier hasn't been set. */
    multiplier = 1;
  }
  else
  {
    multiplier = (pRec->attemptMult * pSmpCfg->attemptExp);
  }

  if ((pSmpCfg->attemptTimeout * multiplier) <= pSmpCfg->maxAttemptTimeout)
  {
    pRec->lockMs = pSmpCfg->attemptTimeout * multiplier;
    pRec->attemptMult = multiplier;
  }
  else
  {
    /* Exponential increase is greater than max timeout. */
    pRec->lockMs = pSmpCfg->maxAttemptTimeout;
  }

  pRec->expDecrementMs = pSmpCfg->attemptDecTimeout;

  /* Ensure the service timer is running. */
  smpDbStartServiceTimer();

  return pRec->lockMs;
}

/*************************************************************************************************/
/*!
 *  \brief  Called to report to the SMP DB that a pairing operation failed
 *
 *  \param  connId    Connection identifier.
 *
 *  \return None.
 */
/*************************************************************************************************/
void SmpDbPairingFailed(dmConnId_t connId)
{
  smpDbDevice_t *pRec = smpDbGetRecord(connId);

  SMP_TRACE_INFO1("SmpDbPairingFailed: connId: %d", connId);

  /* Reset exponent decrement timer. */
  pRec->expDecrementMs = pSmpCfg->attemptDecTimeout;
}

/*************************************************************************************************/
/*!
 *  \brief  Service the device database timers
 *
 *  \return None.
 */
/*************************************************************************************************/
void SmpDbService(void)
{
  uint8_t i;
  smpDbDevice_t *pRec = smpDbCb.db;

  /* Service device specific records. */
  for (i = 0; i < SMP_DB_MAX_DEVICES; i++, pRec++)
  {
    if (smpDbRecordInUse(pRec))
    {
      /* Decrement all time periods. */
      SMP_DB_DEC_TIMER(pRec->expDecrementMs);
      SMP_DB_DEC_TIMER(pRec->lockMs);
      SMP_DB_DEC_TIMER(pRec->failCountToMs);

      /* Process expDecrementMs timeout. */
      if (pRec->expDecrementMs == 0)
      {
        /* Exponential decrease of multiplier. */
        pRec->attemptMult /= pSmpCfg->attemptExp;

        if (pRec->attemptMult)
        {
          pRec->expDecrementMs =  pSmpCfg->attemptDecTimeout;
        }
      }

      /* Process failCountToMs timeout. */
      if (pRec->failCountToMs == 0)
      {
        pRec->failCount = 0;
      }

      /* If the record is in use, ensure the service timer is running. */
      if (smpDbRecordInUse(pRec))
      {
        smpDbStartServiceTimer();
      }
    }
  }
}

/*************************************************************************************************/
/*!
 *  \brief  Remove all records from the SMP database.
 *
 *  \return None.
 */
/*************************************************************************************************/
void SmpDbRemoveAllDevices(void)
{
  /* Stop active service timer. */
  if (smpDbCb.serviceTimer.isStarted == TRUE)
  {
    WsfTimerStop(&smpDbCb.serviceTimer);
  }

  /* Reset database. */
  memset(&smpDbCb.db, 0, sizeof(smpDbCb.db));
}

/*************************************************************************************************/
/*!
 *  \brief  Remove a device with the given identity from the SMP database.
 *`
 *  \param  pAddr     Pointer to BD Address.
 *  \param  addrType  BD Address type.
 *
 *  \return None.
 */
/*************************************************************************************************/
void SmpDbRemoveDevice(uint8_t *pAddr, uint8_t addrType)
{
  smpDbDevice_t *pRec = &smpDbCb.db[SMP_DB_FIRST_REC];
  uint8_t peerAddrType = DmHostAddrType(addrType);
  uint8_t i;

  for (i = SMP_DB_FIRST_REC; i < SMP_DB_MAX_DEVICES; i++, pRec++)
  {
    if (smpDbRecordInUse(pRec) && (pRec->addrType == peerAddrType) && BdaCmp(pRec->peerAddr, pAddr))
    {
      /* Reset record. */
      memset(pRec, 0, sizeof(smpDbDevice_t));

      return;
    }
  }
}
