/*
 * Copyright (c) 2016, 2018, Arm Limited and affiliates.
 * 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.
 */

/*
 * Define flag IGNORE_SIMULATED_NVM_STORAGE to ignore usage of simulated NVM and use
 * platform specific NVM instead.
 */
#ifndef IGNORE_SIMULATED_NVM_STORAGE

#include <stdlib.h>
#include <string.h>
#include "ns_types.h"
#include "ns_list.h"
#include "nsdynmemLIB.h"
#include "eventOS_event_timer.h"
#include "platform/arm_hal_nvm.h"

#include "ns_trace.h"
#define TRACE_GROUP "rnvm"

/**
* NVM to RAM/heap, targeted for testing.
* Key/values are stored to ns_list. Otherwise functionality should be similar to
* nanostack configuration-store adaptation.
*/

#define NVM_RAM_STANDARD_MALLOC
#ifdef NVM_RAM_STANDARD_MALLOC
#define ALLOC malloc
#define FREE free
#else
#define ALLOC ns_dyn_mem_alloc
#define FREE ns_dyn_mem_free
#endif

/*
 * Data entry stored to NVM
 */
typedef struct {
    char *key;
    uint8_t *data;
    uint32_t data_len;
    ns_list_link_t link;
} nvm_data_entry_t;

/*
 * Client request to NVM
 */
typedef struct {
    nvm_callback *client_cb;            // callback provided by client
    void *client_context;               // context provided by client
    void *client_buffer;             // buffer provided by client
    uint16_t *client_buffer_len;        // buffer length provided by client
    platform_nvm_status client_status;  // status to be returned to client
    ns_list_link_t link;
} nvm_client_req_t;

/*
 * NVM context
 */
typedef struct {
    timeout_t *callback_timer;              // timer handle for informing client
    bool is_initialized;
} nvm_context_t;

static NS_LIST_DEFINE(nvm_entry_list, nvm_data_entry_t, link);
static NS_LIST_DEFINE(nvm_client_req_list, nvm_client_req_t, link);

static nvm_context_t *nvm_context_ptr = NULL;

static void nvm_ram_timer_start(void *ctx);
static void nvm_ram_free_entry(nvm_data_entry_t *entry);


static platform_nvm_status create_client_request(nvm_callback *callback, void *context, void *buf, uint16_t *buf_len, platform_nvm_status status)
{
    nvm_client_req_t *nvm_client_req_ptr;
    nvm_client_req_ptr = ALLOC(sizeof(nvm_client_req_t));
    if (!nvm_client_req_ptr) {
        return PLATFORM_NVM_ERROR;
    }
    nvm_client_req_ptr->client_cb = callback;
    nvm_client_req_ptr->client_context = context;
    nvm_client_req_ptr->client_buffer = buf;
    nvm_client_req_ptr->client_buffer_len = buf_len;
    nvm_client_req_ptr->client_status = status;
    ns_list_add_to_end(&nvm_client_req_list, nvm_client_req_ptr);

    if (nvm_context_ptr->callback_timer == NULL) {
        nvm_ram_timer_start(nvm_client_req_ptr);
    }

    return PLATFORM_NVM_OK;
}

platform_nvm_status platform_nvm_init(nvm_callback *callback, void *context)
{
    if (nvm_context_ptr == NULL) {
        nvm_context_ptr = ALLOC(sizeof(nvm_context_t));
        if (!nvm_context_ptr) {
            return PLATFORM_NVM_ERROR;
        }
        nvm_context_ptr->callback_timer = NULL;
        nvm_context_ptr->is_initialized = true;
        ns_list_init(&nvm_entry_list);
        ns_list_init(&nvm_client_req_list);
    } else {
        if (nvm_context_ptr->is_initialized == true) {
            return PLATFORM_NVM_ERROR;
        }
    }

    return create_client_request(callback, context, NULL, NULL, PLATFORM_NVM_OK);
}

platform_nvm_status platform_nvm_finalize(nvm_callback *callback, void *context)
{
    platform_nvm_status ret;
    if (nvm_context_ptr->is_initialized == false) {
        return PLATFORM_NVM_ERROR;
    }

    ret = create_client_request(callback, context, NULL, NULL, PLATFORM_NVM_OK);
    if (ret == PLATFORM_NVM_OK) {
        nvm_context_ptr->is_initialized = false;
    }

    return ret;
}

platform_nvm_status platform_nvm_key_create(nvm_callback *callback, const char *key_name, uint16_t value_len, uint32_t flags, void *context)
{
    (void)flags;
    tr_debug("platform_nvm_key_create() %s len=%d", key_name, (int)value_len);

    ns_list_foreach(nvm_data_entry_t, current_entry, &nvm_entry_list) {
        if (strcmp(current_entry->key, key_name) == 0) {
            // resizing existing key
            ns_list_remove(&nvm_entry_list, current_entry);
            nvm_ram_free_entry(current_entry);
            break;
        }
    }

    nvm_data_entry_t *entry = ALLOC(sizeof(nvm_data_entry_t));
    if (!entry) {
        return PLATFORM_NVM_ERROR;
    }
    memset(entry, 0, sizeof(nvm_data_entry_t));
    size_t key_len = strlen(key_name) + 1;
    entry->key = ALLOC(key_len);
    if (!entry->key) {
        FREE(entry);
        return PLATFORM_NVM_ERROR;
    }
    memcpy(entry->key, key_name, key_len);
    entry->data_len = value_len;
    entry->data = ALLOC(value_len);
    if (!entry->data) {
        FREE(entry->key);
        FREE(entry);
        return PLATFORM_NVM_ERROR;
    }

    ns_list_add_to_end(&nvm_entry_list, entry);

    return create_client_request(callback, context, NULL, NULL, PLATFORM_NVM_OK);
}

platform_nvm_status platform_nvm_key_delete(nvm_callback *callback, const char *key_name, void *context)
{
    platform_nvm_status client_status = PLATFORM_NVM_KEY_NOT_FOUND;
    tr_debug("platform_nvm_key_delete() %s", key_name);

    ns_list_foreach(nvm_data_entry_t, current_entry, &nvm_entry_list) {
        if (strcmp(current_entry->key, key_name) == 0) {
            client_status = PLATFORM_NVM_OK;
            ns_list_remove(&nvm_entry_list, current_entry);
            nvm_ram_free_entry(current_entry);
            break;
        }
    }

    return create_client_request(callback, context, NULL, NULL, client_status);
}

platform_nvm_status platform_nvm_write(nvm_callback *callback, const char *key_name, const void *data, uint16_t *data_len, void *context)
{
    platform_nvm_status client_status = PLATFORM_NVM_KEY_NOT_FOUND;
    tr_debug("platform_nvm_write() %s len=%d", key_name, (int)*data_len);

    ns_list_foreach(nvm_data_entry_t, current_entry, &nvm_entry_list) {
        if (strcmp(current_entry->key, key_name) == 0) {
            if (current_entry->data_len >= *data_len) {
                memcpy(current_entry->data, data, *data_len);
            } else {
                memcpy(current_entry->data, data, current_entry->data_len);
                *data_len = current_entry->data_len;
            }
            client_status = PLATFORM_NVM_OK;
            break;
        }
    }

    return create_client_request(callback, context, (void *)data, data_len, client_status);
}

platform_nvm_status platform_nvm_read(nvm_callback *callback, const char *key_name, void *buf, uint16_t *buf_len, void *context)
{
    platform_nvm_status client_status = PLATFORM_NVM_KEY_NOT_FOUND;
    tr_debug("platform_nvm_read() %s len=%d", key_name, (int)*buf_len);

    ns_list_foreach(nvm_data_entry_t, current_entry, &nvm_entry_list) {
        if (strcmp(current_entry->key, key_name) == 0) {
            if (*buf_len >= current_entry->data_len) {
                memcpy(buf, current_entry->data, current_entry->data_len);
                *buf_len = current_entry->data_len;
            } else {
                memcpy(buf, current_entry->data, *buf_len);
            }
            client_status = PLATFORM_NVM_OK;
            break;
        }
    }

    return create_client_request(callback, context, buf, buf_len, client_status);
}

platform_nvm_status platform_nvm_flush(nvm_callback *callback, void *context)
{
    tr_debug("platform_nvm_flush()");
    return create_client_request(callback, context, NULL, NULL, PLATFORM_NVM_OK);
}

static void nvm_ram_timer_cb(void *args)
{
    nvm_client_req_t *nvm_client_req_ptr = (nvm_client_req_t *)args;
    nvm_client_req_ptr->client_cb(nvm_client_req_ptr->client_status, nvm_client_req_ptr->client_context);
    ns_list_remove(&nvm_client_req_list, nvm_client_req_ptr);
    FREE(nvm_client_req_ptr);

    nvm_context_ptr->callback_timer = NULL;

    if (!ns_list_is_empty(&nvm_client_req_list)) {
        // there are more client requests to process
        nvm_client_req_ptr = ns_list_get_first(&nvm_client_req_list);
        nvm_ram_timer_start(nvm_client_req_ptr);
    }
}

static void nvm_ram_timer_start(void *ctx)
{
    nvm_context_ptr->callback_timer = eventOS_timeout_ms(nvm_ram_timer_cb, 50, ctx);
}

static void nvm_ram_free_entry(nvm_data_entry_t *entry)
{
    FREE(entry->key);
    FREE(entry->data);
    FREE(entry);
}

#endif /* IGNORE_SIMULATED_NVM_STORAGE */
