#pragma once

#include <Arduino.h>
#include <string.h>

#include "esp_err.h"
#include "esp_mac.h"

#ifdef RAM_PREFERENCES
#include "hash_preferences.hpp"
#else
#include <Preferences.h>
#endif

#include <uavcan/node/GetInfo_1_0.h>
#include <uavcan/_register/Value_1_0.h>
#include <uavcan/_register/Name_1_0.h>


static Preferences preferences;
static bool preferences_open = false;

#define REGISTER_LIST_KEY "__reglist"
#define REGISTER_LIST_DELIM "|"

String hash(const char *str)
{
    unsigned long hash = 5381;
    int c;

    while (c = *(str)++)
        hash = ((hash << 5) + hash) + c; /* hash * 33 + c */

    return String(hash, HEX);
}

void open_preferences_if_needed() {
    if(!preferences_open) {
        preferences.begin("uavcan");
        preferences_open = true;
    }
}

void close_preferences_if_needed() {
    if(preferences_open) {
        preferences.end();
        preferences_open = false;
    }
}

void registerWrite(const char *const register_name, const uavcan_register_Value_1_0 *const value)
{
    open_preferences_if_needed();
    int bytes_read = preferences.putBytes(hash(register_name).c_str(), (uint8_t *)value, sizeof(*value));
    String reglist = preferences.getString(REGISTER_LIST_KEY, "");
    String search = String(register_name) + REGISTER_LIST_DELIM;
    if (reglist.indexOf(search) == -1)
    {
        reglist += search;
        preferences.putString(REGISTER_LIST_KEY, reglist);
    }
    close_preferences_if_needed();
}

void registerRead(const char *const register_name, uavcan_register_Value_1_0 *const value)
{
    open_preferences_if_needed();
    if (preferences.isKey(hash(register_name).c_str()))
    {
        preferences.getBytes(hash(register_name).c_str(), (uint8_t *)value, sizeof(*value));
    }
    else
    {
        registerWrite(register_name, value);
    }
    close_preferences_if_needed();
}

uavcan_register_Name_1_0 registerGetNameByIndex(const uint16_t index)
{
    uavcan_register_Name_1_0 name;
    name.name.elements[0] = '\0';
    name.name.count = 0;

    open_preferences_if_needed();
    String reglist = preferences.getString(REGISTER_LIST_KEY, "");

    int start = 0;
    int found = 0;
    while (found <= index)
    {
        int end = reglist.indexOf(REGISTER_LIST_DELIM, start);
        if (end == -1)
            break;

        if (found == index)
        {
            String reg = reglist.substring(start, end);
            strcpy((char *)name.name.elements, reg.c_str());
            name.name.count = reg.length();
            break;
        }

        start = end + 1;
        found++;
    }
    close_preferences_if_needed();
    return name;
}

bool registerAssign(uavcan_register_Value_1_0 *const dst, const uavcan_register_Value_1_0 *const src)
{
    if (dst->_tag_ == src->_tag_ || uavcan_register_Value_1_0_is_empty_(dst))
    {
        *dst = *src;
        return true;
    }
    return false;
}

#include <nvs_flash.h>

void registerDoFactoryReset(void)
{
    nvs_flash_erase();      // erase the NVS partition and...
    nvs_flash_init();       // initialize the NVS partition.
    esp_restart();
}

// Returns the 128-bit unique-ID of the local node. This value is used in uavcan.node.GetInfo.Response and during the
// plug-and-play node-ID allocation by uavcan.pnp.NodeIDAllocationData. The function is infallible.
void getUniqueID(uint8_t out[uavcan_node_GetInfo_Response_1_0_unique_id_ARRAY_CAPACITY_])
{
    // A real hardware node would read its unique-ID from some hardware-specific source (typically stored in ROM).
    // This example is a software-only node, so we store the unique-ID in a (read-only) register instead.
    uavcan_register_Value_1_0 value = {0};
    uavcan_register_Value_1_0_select_unstructured_(&value);
    if(esp_base_mac_addr_get(&value.unstructured.value.elements[0]) == ESP_OK) {
        value.unstructured.value.count = 8;
    }
    for (uint8_t i = value.unstructured.value.count; i < uavcan_node_GetInfo_Response_1_0_unique_id_ARRAY_CAPACITY_; i += 1)
    {
        value.unstructured.value.elements[value.unstructured.value.count++] = (uint8_t)rand(); // NOLINT
    }
    // Populate the default; it is only used at the first run if there is no such register.
    
    registerRead("uavcan.node.unique_id", &value);
    assert(uavcan_register_Value_1_0_is_unstructured_(&value) &&
           value.unstructured.value.count == uavcan_node_GetInfo_Response_1_0_unique_id_ARRAY_CAPACITY_);
    memcpy(&out[0], &value.unstructured.value, uavcan_node_GetInfo_Response_1_0_unique_id_ARRAY_CAPACITY_);
}

