/* Copyright (c) 2015 Nordic Semiconductor. All Rights Reserved.
 *
 * The information contained herein is property of Nordic Semiconductor ASA.
 * Terms and conditions of usage are described in detail in NORDIC
 * SEMICONDUCTOR STANDARD SOFTWARE LICENSE AGREEMENT.
 *
 * Licensees are granted free, non-transferable use of the information. NO
 * WARRANTY of ANY KIND is provided. This heading must NOT be removed from
 * the file.
 *
 */

#include "sdk_common.h"
#if NRF_MODULE_ENABLED(PEER_MANAGER)
#include "peer_data_storage.h"

#include <stdint.h>
#include <string.h>
#include "sdk_errors.h"
#include "peer_manager_types.h"
#include "peer_manager_internal.h"
#include "peer_id.h"
#include "peer_data.h"
#include "fds.h"


// Macro for verifying that the peer id is within a valid range.
#define VERIFY_PEER_ID_IN_RANGE(id)         VERIFY_FALSE((id >= PM_PEER_ID_N_AVAILABLE_IDS), \
                                            NRF_ERROR_INVALID_PARAM)

// Macro for verifying that the peer data id is withing a valid range.
#define VERIFY_PEER_DATA_ID_IN_RANGE(id)    VERIFY_TRUE(peer_data_id_is_valid(id), \
                                            NRF_ERROR_INVALID_PARAM)

// The number of registered event handlers.
#define PDS_EVENT_HANDLERS_CNT              (sizeof(m_evt_handlers) / sizeof(m_evt_handlers[0]))


// Peer Data Storage event handler in Peer Database.
extern void pdb_pds_evt_handler(pds_evt_t const *);

// Peer Data Storage events' handlers.
// The number of elements in this array is PDS_EVENT_HANDLERS_CNT.
static pds_evt_handler_t const m_evt_handlers[] =
{
    pdb_pds_evt_handler,
};

static bool m_module_initialized  = false;
static bool m_peer_delete_queued  = false;
static bool m_peer_delete_ongoing = false;

// A token used for Flash Data Storage searches.
static fds_find_token_t m_fds_ftok;


// Function for dispatching events to all registered event handlers.
static void pds_evt_send(pds_evt_t * p_event)
{
    for (uint32_t i = 0; i < PDS_EVENT_HANDLERS_CNT; i++)
    {
        m_evt_handlers[i](p_event);
    }
}


// Function to convert peer IDs to file IDs.
static uint16_t peer_id_to_file_id(pm_peer_id_t peer_id)
{
    return (uint16_t)(peer_id + PEER_ID_TO_FILE_ID);
}


// Function to convert peer data id to type id.
static pm_peer_id_t file_id_to_peer_id(uint16_t file_id)
{
    return (pm_peer_id_t)(file_id + FILE_ID_TO_PEER_ID);
}


// Function to convert peer data IDs to record keys.
static uint16_t peer_data_id_to_record_key(pm_peer_data_id_t peer_data_id)
{
    return (uint16_t)(peer_data_id + DATA_ID_TO_RECORD_KEY);
}


// Function to convert record keys to peer data IDs.
static pm_peer_data_id_t record_key_to_peer_data_id(uint16_t record_key)
{
    return (pm_peer_data_id_t)(record_key + RECORD_KEY_TO_DATA_ID);
}


// Function for checking whether a file ID is relevant for the Peer Manager.
static bool file_id_within_pm_range(uint16_t file_id)
{
    return ((PDS_FIRST_RESERVED_FILE_ID <= file_id)
         && (file_id <= PDS_LAST_RESERVED_FILE_ID));
}


// Function for checking whether a record key is relevant for the Peer Manager.
static bool record_key_within_pm_range(uint16_t record_key)
{
    return ((PDS_FIRST_RESERVED_RECORD_KEY <= record_key)
         && (record_key <= PDS_LAST_RESERVED_RECORD_KEY));
}


static bool peer_data_id_is_valid(pm_peer_data_id_t data_id)
{
    return ((data_id == PM_PEER_DATA_ID_BONDING)                 ||
            (data_id == PM_PEER_DATA_ID_SERVICE_CHANGED_PENDING) ||
            (data_id == PM_PEER_DATA_ID_GATT_LOCAL)              ||
            (data_id == PM_PEER_DATA_ID_GATT_REMOTE)             ||
            (data_id == PM_PEER_DATA_ID_PEER_RANK)               ||
            (data_id == PM_PEER_DATA_ID_APPLICATION));
}


// Function for deleting all data beloning to a peer.
// These operations will be sent to FDS one at a time.
static void peer_data_delete()
{
    ret_code_t        ret;
    pm_peer_id_t      peer_id;
    uint16_t          file_id;
    fds_record_desc_t desc;
    fds_find_token_t  ftok;

    memset(&ftok, 0x00, sizeof(fds_find_token_t));
    peer_id = peer_id_get_next_deleted(PM_PEER_ID_INVALID);

    while (    (peer_id != PM_PEER_ID_INVALID)
            && (fds_record_find_in_file(peer_id_to_file_id(peer_id), &desc, &ftok)
            == FDS_ERR_NOT_FOUND))
    {
        peer_id_free(peer_id);
        peer_id = peer_id_get_next_deleted(peer_id);
    }

    if (!m_peer_delete_ongoing && (peer_id != PM_PEER_ID_INVALID))
    {
        m_peer_delete_ongoing = true;

        file_id = peer_id_to_file_id(peer_id);
        ret     = fds_file_delete(file_id);

        if (ret == FDS_ERR_NO_SPACE_IN_QUEUES)
        {
            m_peer_delete_queued = true;
        }
        else if (ret != NRF_SUCCESS)
        {
            m_peer_delete_ongoing = false;

            pds_evt_t pds_evt;

            pds_evt.evt_id      = PDS_EVT_ERROR_UNEXPECTED;
            pds_evt.peer_id     = peer_id;
            pds_evt.data_id     = PM_PEER_DATA_ID_INVALID;
            pds_evt.store_token = PM_STORE_TOKEN_INVALID;
            pds_evt.result      = ret;

            pds_evt_send(&pds_evt);
        }
    }
}


static ret_code_t peer_data_find(pm_peer_id_t              peer_id,
                                 pm_peer_data_id_t         data_id,
                                 fds_record_desc_t * const p_desc)
{
    ret_code_t       ret;
    fds_find_token_t ftok;

    NRF_PM_DEBUG_CHECK(peer_id < PM_PEER_ID_N_AVAILABLE_IDS);
    NRF_PM_DEBUG_CHECK(peer_data_id_is_valid(data_id));
    NRF_PM_DEBUG_CHECK(p_desc != NULL);

    memset(&ftok, 0x00, sizeof(fds_find_token_t));

    uint16_t file_id    = peer_id_to_file_id(peer_id);
    uint16_t record_key = peer_data_id_to_record_key(data_id);

    ret = fds_record_find(file_id, record_key, p_desc, &ftok);

    if (ret != FDS_SUCCESS)
    {
        return NRF_ERROR_NOT_FOUND;
    }

    return NRF_SUCCESS;
}


static void peer_ids_load()
{
    fds_record_desc_t  record_desc;
    fds_flash_record_t record;
    fds_find_token_t   ftok;

    memset(&ftok, 0x00, sizeof(fds_find_token_t));

    uint16_t const record_key = peer_data_id_to_record_key(PM_PEER_DATA_ID_BONDING);

    while (fds_record_find_by_key(record_key, &record_desc, &ftok) == FDS_SUCCESS)
    {
        pm_peer_id_t peer_id;

        // It is safe to ignore the return value since the descriptor was
        // just obtained and also 'record' is different from NULL.
        (void)fds_record_open(&record_desc, &record);
        peer_id = file_id_to_peer_id(record.p_header->ic.file_id);
        (void)fds_record_close(&record_desc);

        (void)peer_id_allocate(peer_id);
    }
}


static void fds_evt_handler(fds_evt_t const * const p_fds_evt)
{
    pds_evt_t pds_evt;

    pds_evt.result = (p_fds_evt->result == FDS_SUCCESS);

    switch (p_fds_evt->id)
    {
        case FDS_EVT_WRITE:
        case FDS_EVT_UPDATE:
            if (   file_id_within_pm_range(p_fds_evt->write.file_id)
                || record_key_within_pm_range(p_fds_evt->write.record_key))
            {
                pds_evt.peer_id = file_id_to_peer_id(p_fds_evt->write.file_id);
                pds_evt.data_id = record_key_to_peer_data_id(p_fds_evt->write.record_key);

                if (p_fds_evt->id == FDS_EVT_WRITE)
                {
                    pds_evt.evt_id = (p_fds_evt->result == FDS_SUCCESS) ? PDS_EVT_STORED :
                                                                          PDS_EVT_ERROR_STORE;
                }
                else
                {
                    pds_evt.evt_id = (p_fds_evt->result == FDS_SUCCESS) ? PDS_EVT_UPDATED :
                                                                          PDS_EVT_ERROR_UPDATE;
                }

                pds_evt.result      = p_fds_evt->result;
                pds_evt.store_token = p_fds_evt->write.record_id;

                pds_evt_send(&pds_evt);
            }
            break;

        case FDS_EVT_DEL_RECORD:
            if (   file_id_within_pm_range(p_fds_evt->del.file_id)
                || record_key_within_pm_range(p_fds_evt->del.record_key))
            {
                pds_evt.peer_id = file_id_to_peer_id(p_fds_evt->del.file_id);
                pds_evt.data_id = record_key_to_peer_data_id(p_fds_evt->del.record_key);

                pds_evt.evt_id = (p_fds_evt->result == FDS_SUCCESS) ? PDS_EVT_CLEARED :
                                                                      PDS_EVT_ERROR_CLEAR;

                pds_evt.store_token = p_fds_evt->del.record_id;

                pds_evt_send(&pds_evt);
            }
            break;

        case FDS_EVT_DEL_FILE:
            {
                if (    file_id_within_pm_range(p_fds_evt->del.file_id)
                    && (p_fds_evt->del.record_key == FDS_RECORD_KEY_DIRTY))
                {
                    pds_evt.peer_id = file_id_to_peer_id(p_fds_evt->del.file_id);
                    pds_evt.data_id = record_key_to_peer_data_id(p_fds_evt->del.record_key);

                    pds_evt.data_id = PM_PEER_DATA_ID_INVALID;
                    if (p_fds_evt->result == FDS_SUCCESS)
                    {
                        pds_evt.evt_id = PDS_EVT_PEER_ID_CLEAR;
                        peer_id_free(pds_evt.peer_id);
                    }
                    else
                    {
                        pds_evt.evt_id = PDS_EVT_ERROR_PEER_ID_CLEAR;
                    }

                    m_peer_delete_queued  = false;
                    m_peer_delete_ongoing = false;

                    peer_data_delete();

                    pds_evt_send(&pds_evt);
                }
            }
            break;

        case FDS_EVT_GC:
            pds_evt.evt_id = PDS_EVT_COMPRESSED;
            pds_evt_send(&pds_evt);
            break;

        default:
            break;
    }

    if (m_peer_delete_queued)
    {
        m_peer_delete_queued  = false;
        peer_data_delete();
    }
}


ret_code_t pds_init()
{
    ret_code_t ret;

    // Check for re-initialization if debugging.
    NRF_PM_DEBUG_CHECK(!m_module_initialized);

    ret = fds_register(fds_evt_handler);
    if (ret != NRF_SUCCESS)
    {
        return NRF_ERROR_INTERNAL;
    }

    ret = fds_init();
    if (ret != NRF_SUCCESS)
    {
        return NRF_ERROR_STORAGE_FULL;
    }

    peer_id_init();
    peer_ids_load();

    m_module_initialized = true;

    return NRF_SUCCESS;
}


ret_code_t pds_peer_data_read(pm_peer_id_t                    peer_id,
                              pm_peer_data_id_t               data_id,
                              pm_peer_data_t          * const p_data,
                              uint32_t          const * const p_buf_len)
{
    ret_code_t         ret;
    fds_record_desc_t  rec_desc;
    fds_flash_record_t rec_flash;

    NRF_PM_DEBUG_CHECK(m_module_initialized);
    NRF_PM_DEBUG_CHECK(p_data != NULL);

    VERIFY_PEER_ID_IN_RANGE(peer_id);
    VERIFY_PEER_DATA_ID_IN_RANGE(data_id);

    ret = peer_data_find(peer_id, data_id, &rec_desc);

    if (ret != NRF_SUCCESS)
    {
        return NRF_ERROR_NOT_FOUND;
    }

    // Shouldn't fail, unless the record was deleted in the meanwhile or the CRC check has failed.
    ret = fds_record_open(&rec_desc, &rec_flash);

    if (ret != NRF_SUCCESS)
    {
        return NRF_ERROR_NOT_FOUND;
    }

    // @note emdi: could this actually be set by the caller and used instead
    // of an additional parameter (data_id) ?
    p_data->data_id      = data_id;
    p_data->length_words = rec_flash.p_header->tl.length_words;

    // If p_buf_len is NULL, provide a pointer to data in flash, otherwise,
    // check that the buffer is large enough and copy the data in flash into the buffer.
    if (p_buf_len != NULL)
    {
        uint32_t const data_len_bytes = (p_data->length_words * sizeof(uint32_t));

        if ((*p_buf_len) <= data_len_bytes)
        {
            memcpy(p_data->p_all_data, rec_flash.p_data, data_len_bytes);
        }
        else
        {
            return NRF_ERROR_NO_MEM;
        }
    }
    else
    {
        // The cast is necessary because if no buffer is provided, we just copy the pointer,
        // but it that case it should be considered a pointer to const data by the caller,
        // since it is a pointer to data in flash.
        p_data->p_all_data = (void*)rec_flash.p_data;
    }

    // Shouldn't fail unless the record was already closed, in which case it can be ignored.
    (void)fds_record_close(&rec_desc);

    return NRF_SUCCESS;
}


void pds_peer_data_iterate_prepare(void)
{
    memset(&m_fds_ftok, 0x00, sizeof(fds_find_token_t));
}


bool pds_peer_data_iterate(pm_peer_data_id_t            data_id,
                           pm_peer_id_t         * const p_peer_id,
                           pm_peer_data_flash_t * const p_data)
{
    ret_code_t         ret;
    uint16_t           rec_key;
    fds_record_desc_t  rec_desc;
    fds_flash_record_t rec_flash;

    NRF_PM_DEBUG_CHECK(m_module_initialized);
    NRF_PM_DEBUG_CHECK(p_peer_id != NULL);
    NRF_PM_DEBUG_CHECK(p_data    != NULL);

    // @note emdi: should we check the data_id ?
    rec_key = peer_data_id_to_record_key(data_id);

    if (fds_record_find_by_key(rec_key, &rec_desc, &m_fds_ftok) != NRF_SUCCESS)
    {
        return false;
    }

    ret = fds_record_open(&rec_desc, &rec_flash);

    if (ret != NRF_SUCCESS)
    {
        // It can only happen if the record was deleted after the call to fds_record_find_by_key(),
        // before we could open it, or if CRC support was enabled in Flash Data Storage at compile
        // time and the CRC check failed.
        return false;
    }

    p_data->data_id      = data_id;
    p_data->length_words = rec_flash.p_header->tl.length_words;
    p_data->p_all_data   = rec_flash.p_data;

    *p_peer_id           = file_id_to_peer_id(rec_flash.p_header->ic.file_id);

    (void)fds_record_close(&rec_desc);

    return true;
}


ret_code_t pds_space_reserve(pm_peer_data_const_t const * p_peer_data,
                             pm_prepare_token_t         * p_prepare_token)
{
    ret_code_t ret;

    NRF_PM_DEBUG_CHECK(m_module_initialized);
    NRF_PM_DEBUG_CHECK(p_peer_data     != NULL);
    NRF_PM_DEBUG_CHECK(p_prepare_token != NULL);

    VERIFY_PEER_DATA_ID_IN_RANGE(p_peer_data->data_id);

    ret = fds_reserve((fds_reserve_token_t*)p_prepare_token, p_peer_data->length_words);

    switch (ret)
    {
        case FDS_SUCCESS:
            return NRF_SUCCESS;

        case FDS_ERR_RECORD_TOO_LARGE:
            return NRF_ERROR_INVALID_LENGTH;

        case FDS_ERR_NO_SPACE_IN_FLASH:
            return NRF_ERROR_STORAGE_FULL;

        default:
            return NRF_ERROR_INTERNAL;
    }
}


ret_code_t pds_space_reserve_cancel(pm_prepare_token_t prepare_token)
{
    ret_code_t ret;

    NRF_PM_DEBUG_CHECK(m_module_initialized);
    NRF_PM_DEBUG_CHECK(prepare_token != PDS_PREPARE_TOKEN_INVALID);

    ret = fds_reserve_cancel((fds_reserve_token_t*)&prepare_token);

    if (ret != FDS_SUCCESS)
    {
        return NRF_ERROR_INTERNAL;
    }

    return NRF_SUCCESS;
}


ret_code_t pds_peer_data_store(pm_peer_id_t                 peer_id,
                               pm_peer_data_const_t const * p_peer_data,
                               pm_prepare_token_t           prepare_token,
                               pm_store_token_t           * p_store_token)
{
    ret_code_t         ret;
    fds_record_t       rec;
    fds_record_desc_t  rec_desc;
    fds_record_chunk_t rec_chunk;

    NRF_PM_DEBUG_CHECK(m_module_initialized);
    NRF_PM_DEBUG_CHECK(p_peer_data != NULL);

    VERIFY_PEER_ID_IN_RANGE(peer_id);
    VERIFY_PEER_DATA_ID_IN_RANGE(p_peer_data->data_id);

    // Prepare chunk.
    rec_chunk.p_data       = p_peer_data->p_all_data;
    rec_chunk.length_words = p_peer_data->length_words;

    // Prepare the record to be stored in flash.
    rec.file_id         = peer_id_to_file_id(peer_id);
    rec.key             = peer_data_id_to_record_key(p_peer_data->data_id);
    rec.data.p_chunks   = &rec_chunk;
    rec.data.num_chunks = 1;

    ret = peer_data_find(peer_id, p_peer_data->data_id, &rec_desc);

    if (ret == NRF_ERROR_NOT_FOUND)
    {
        // No previous data exists in flash.
        if (prepare_token == PDS_PREPARE_TOKEN_INVALID)
        {
            // No space was previously reserved.
            ret = fds_record_write(&rec_desc, &rec);
        }
        else
        {
            // Space for this record was previously reserved.
            ret = fds_record_write_reserved(&rec_desc, &rec, (fds_reserve_token_t*)&prepare_token);
        }
    }
    else // NRF_SUCCESS
    {
        // Token shouldn't be zero at this point.
        // NRF_PM_DEBUG_CHECK(prepare_token != PDS_PREPARE_TOKEN_INVALID);

        (void)fds_reserve_cancel((fds_reserve_token_t*)&prepare_token);

        // Update existing record.
        ret = fds_record_update(&rec_desc, &rec);
    }

    switch (ret)
    {
        case FDS_SUCCESS:
            if (p_store_token != NULL)
            {
                // Update the store token.
                (void)fds_record_id_from_desc(&rec_desc, (uint32_t*)p_store_token);
            }
            return NRF_SUCCESS;

        case FDS_ERR_BUSY:
        case FDS_ERR_NO_SPACE_IN_QUEUES:
            return NRF_ERROR_BUSY;

        case FDS_ERR_NO_SPACE_IN_FLASH:
            return NRF_ERROR_STORAGE_FULL;

        default:
            return NRF_ERROR_INTERNAL;
    }
}


// @note emdi: unused..
ret_code_t pds_peer_data_delete(pm_peer_id_t peer_id, pm_peer_data_id_t data_id)
{
    ret_code_t        ret;
    fds_record_desc_t record_desc;

    NRF_PM_DEBUG_CHECK(m_module_initialized);

    VERIFY_PEER_ID_IN_RANGE(peer_id);
    VERIFY_PEER_DATA_ID_IN_RANGE(data_id);

    ret = peer_data_find(peer_id, data_id, &record_desc);

    if (ret != NRF_SUCCESS)
    {
        return NRF_ERROR_NOT_FOUND;
    }

    ret = fds_record_delete(&record_desc);

    switch (ret)
    {
        case FDS_SUCCESS:
            return NRF_SUCCESS;

        case FDS_ERR_NO_SPACE_IN_QUEUES:
            return NRF_ERROR_BUSY;

        default:
            return NRF_ERROR_INTERNAL;
    }
}


pm_peer_id_t pds_peer_id_allocate(void)
{
    NRF_PM_DEBUG_CHECK(m_module_initialized);
    return peer_id_allocate(PM_PEER_ID_INVALID);
}


ret_code_t pds_peer_id_free(pm_peer_id_t peer_id)
{
    NRF_PM_DEBUG_CHECK(m_module_initialized);
    VERIFY_PEER_ID_IN_RANGE(peer_id);

    (void)peer_id_delete(peer_id);
    peer_data_delete();

    return NRF_SUCCESS;
}


bool pds_peer_id_is_allocated(pm_peer_id_t peer_id)
{
    NRF_PM_DEBUG_CHECK(m_module_initialized);
    return peer_id_is_allocated(peer_id);
}


pm_peer_id_t pds_next_peer_id_get(pm_peer_id_t prev_peer_id)
{
    NRF_PM_DEBUG_CHECK(m_module_initialized);
    return peer_id_get_next_used(prev_peer_id);
}


pm_peer_id_t pds_next_deleted_peer_id_get(pm_peer_id_t prev_peer_id)
{
    NRF_PM_DEBUG_CHECK(m_module_initialized);
    return peer_id_get_next_deleted(prev_peer_id);
}


uint32_t pds_peer_count_get(void)
{
    NRF_PM_DEBUG_CHECK(m_module_initialized);
    return peer_id_n_ids();
}
#endif // NRF_MODULE_ENABLED(PEER_MANAGER)
