/*
 * SPDX-FileCopyrightText: 2019-2022 Espressif Systems (Shanghai) CO LTD
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include <esp_err.h>
#include <esp_log.h>

#include <protocomm.h>
#include <protocomm_security0.h>
#include <protocomm_security1.h>
#include <protocomm_security2.h>

#include <esp_local_ctrl.h>
#include "esp_local_ctrl_priv.h"
#include "esp_local_ctrl.pb-c.h"

#define ESP_LOCAL_CTRL_VERSION "v1.0"

struct inst_ctx {
    protocomm_t *pc;
    esp_local_ctrl_config_t config;
    esp_local_ctrl_prop_t **props;
    size_t props_count;
};

struct inst_ctx *local_ctrl_inst_ctx;

static const char *TAG = "esp_local_ctrl";

esp_err_t esp_local_ctrl_start(const esp_local_ctrl_config_t *config)
{
    esp_err_t ret;

    if (!config) {
        ESP_LOGE(TAG, "NULL configuration provided");
        return ESP_ERR_INVALID_ARG;
    }

    if (!config->transport) {
        ESP_LOGE(TAG, "No transport provided");
        return ESP_ERR_INVALID_ARG;
    }

    if (config->max_properties == 0) {
        ESP_LOGE(TAG, "max_properties must be greater than 0");
        return ESP_ERR_INVALID_ARG;
    }

    if (!config->handlers.get_prop_values ||
        !config->handlers.set_prop_values) {
        ESP_LOGE(TAG, "Handlers cannot be null");
        return ESP_ERR_INVALID_ARG;
    }

    if (local_ctrl_inst_ctx) {
        ESP_LOGW(TAG, "Service already active");
        return ESP_ERR_INVALID_STATE;
    }

    local_ctrl_inst_ctx = calloc(1, sizeof(struct inst_ctx));
    if (!local_ctrl_inst_ctx) {
        ESP_LOGE(TAG, "Failed to allocate memory for instance");
        return ESP_ERR_NO_MEM;
    }
    memcpy(&local_ctrl_inst_ctx->config, config, sizeof(local_ctrl_inst_ctx->config));

    local_ctrl_inst_ctx->props = calloc(local_ctrl_inst_ctx->config.max_properties,
                                        sizeof(esp_local_ctrl_prop_t *));
    if (!local_ctrl_inst_ctx->props) {
        ESP_LOGE(TAG, "Failed to allocate memory for properties");
        free(local_ctrl_inst_ctx);
        local_ctrl_inst_ctx = NULL;
        return ESP_ERR_NO_MEM;
    }

    /* Since the config structure will be different for different transport modes, each transport may
     * implement a `copy_config()` function, which accepts a configuration structure as input and
     * creates a copy of that, which can be kept in the context structure of the `esp_local_ctrl` instance.
     * This copy can be later be freed using `free_config()` */
    if (config->transport->copy_config) {
        ret = config->transport->copy_config(&local_ctrl_inst_ctx->config.transport_config,
                                             &config->transport_config);
        if (ret != ESP_OK) {
            esp_local_ctrl_stop();
            return ret;
        }
    }

    /* For a selected transport mode, endpoints may need to be declared prior to starting the
     * `esp_local_ctrl` service, e.g. in case of BLE. By declaration it means that the transport layer
     * allocates some resources for an endpoint, and later, after service has started, a handler
     * is assigned for that endpoint */
    if (config->transport->declare_ep) {
        /* UUIDs are 16bit unique IDs for each endpoint. This may or may not be relevant for
         * a chosen transport. We reserve all values from FF50 to FFFF for the internal endpoints.
         * The remaining endpoints can be used by the application for its own custom endpoints */
        uint16_t start_uuid = 0xFF50;
        ret = config->transport->declare_ep(&local_ctrl_inst_ctx->config.transport_config,
                                            "esp_local_ctrl/version", start_uuid++);
        if (ret != ESP_OK) {
            esp_local_ctrl_stop();
            return ret;
        }
        ret = config->transport->declare_ep(&local_ctrl_inst_ctx->config.transport_config,
                                            "esp_local_ctrl/session", start_uuid++);
        if (ret != ESP_OK) {
            esp_local_ctrl_stop();
            return ret;
        }
        ret = config->transport->declare_ep(&local_ctrl_inst_ctx->config.transport_config,
                                            "esp_local_ctrl/control", start_uuid++);
        if (ret != ESP_OK) {
            esp_local_ctrl_stop();
            return ret;
        }
    }

    local_ctrl_inst_ctx->pc = protocomm_new();
    if (!local_ctrl_inst_ctx->pc) {
        ESP_LOGE(TAG, "Failed to create new protocomm instance");
        esp_local_ctrl_stop();
        return ESP_FAIL;
    }

    if (config->transport->start_service) {
        ret = config->transport->start_service(local_ctrl_inst_ctx->pc,
                                               &local_ctrl_inst_ctx->config.transport_config);
        if (ret != ESP_OK) {
            esp_local_ctrl_stop();
            return ret;
        }
    }

    ret = protocomm_set_version(local_ctrl_inst_ctx->pc, "esp_local_ctrl/version",
                                ESP_LOCAL_CTRL_VERSION);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Failed to set version endpoint");
        esp_local_ctrl_stop();
        return ret;
    }

    protocomm_security_t *proto_sec_handle = NULL;
    switch (local_ctrl_inst_ctx->config.proto_sec.version) {
        case PROTOCOM_SEC_CUSTOM:
            proto_sec_handle = local_ctrl_inst_ctx->config.proto_sec.custom_handle;
            break;
        case PROTOCOM_SEC1:
#ifdef CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_1
            proto_sec_handle = (protocomm_security_t *) &protocomm_security1;
#else
            // Enable SECURITY_VERSION_1 in Protocomm configuration menu
            return ESP_ERR_NOT_SUPPORTED;
#endif
            break;
        case PROTOCOM_SEC2:
#ifdef CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_2
            proto_sec_handle = (protocomm_security_t *) &protocomm_security2;
            break;
#else
            // Enable SECURITY_VERSION_2 in Protocomm configuration menu
            return ESP_ERR_NOT_SUPPORTED;
#endif
        case PROTOCOM_SEC0:
        default:
#ifdef CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_0
            proto_sec_handle = (protocomm_security_t *) &protocomm_security0;
#else
            // Enable SECURITY_VERSION_0 in Protocomm configuration menu
            return ESP_ERR_NOT_SUPPORTED;
#endif
            break;
    }
    ret = protocomm_set_security(local_ctrl_inst_ctx->pc, "esp_local_ctrl/session",
                                 proto_sec_handle, local_ctrl_inst_ctx->config.proto_sec.sec_params);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Failed to set session endpoint");
        esp_local_ctrl_stop();
        return ret;
    }

    ret = protocomm_add_endpoint(local_ctrl_inst_ctx->pc, "esp_local_ctrl/control",
                                 esp_local_ctrl_data_handler, NULL);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Failed to set control endpoint");
        esp_local_ctrl_stop();
        return ret;
    }
    return ESP_OK;
}

esp_err_t esp_local_ctrl_stop(void)
{
    if (local_ctrl_inst_ctx) {
        if (local_ctrl_inst_ctx->config.transport->free_config) {
            local_ctrl_inst_ctx->config.transport->free_config(&local_ctrl_inst_ctx->config.transport_config);
        }
        if (local_ctrl_inst_ctx->pc) {
            if (local_ctrl_inst_ctx->config.transport->stop_service) {
                local_ctrl_inst_ctx->config.transport->stop_service(local_ctrl_inst_ctx->pc);
            }
            protocomm_delete(local_ctrl_inst_ctx->pc);
        }
        if (local_ctrl_inst_ctx->config.handlers.usr_ctx_free_fn) {
            local_ctrl_inst_ctx->config.handlers.usr_ctx_free_fn(
                local_ctrl_inst_ctx->config.handlers.usr_ctx);
        }

        /* Iterate through all properties one by one and free them */
        for (uint32_t i = 0; i < local_ctrl_inst_ctx->config.max_properties; i++) {
            if (local_ctrl_inst_ctx->props[i] == NULL) {
                continue;
            }
            /* Release memory allocated for property data */
            free(local_ctrl_inst_ctx->props[i]->name);
            if (local_ctrl_inst_ctx->props[i]->ctx_free_fn) {
                local_ctrl_inst_ctx->props[i]->ctx_free_fn(local_ctrl_inst_ctx->props[i]->ctx);
            }
            free(local_ctrl_inst_ctx->props[i]);
        }
        free(local_ctrl_inst_ctx->props);
        free(local_ctrl_inst_ctx);
        local_ctrl_inst_ctx = NULL;
    }
    return ESP_OK;
}

static int esp_local_ctrl_get_property_index(const char *name)
{
    if (!local_ctrl_inst_ctx || !name) {
        return -1;
    }

    /* Iterate through all properties one by one
     * and find the one with matching name */
    for (uint32_t i = 0; i < local_ctrl_inst_ctx->props_count; i++) {
        if (strcmp(local_ctrl_inst_ctx->props[i]->name, name) == 0) {
            return i;
        }
    }
    return -1;
}

esp_err_t esp_local_ctrl_add_property(const esp_local_ctrl_prop_t *prop)
{
    if (!local_ctrl_inst_ctx) {
        ESP_LOGE(TAG, "Service not running");
        return ESP_ERR_INVALID_STATE;
    }
    if (!prop || !prop->name) {
        return ESP_ERR_INVALID_ARG;
    }
    if (esp_local_ctrl_get_property_index(prop->name) >= 0) {
        ESP_LOGE(TAG, "Property with name %s exists", prop->name);
        return ESP_ERR_INVALID_STATE;
    }

    if (local_ctrl_inst_ctx->config.max_properties
        == local_ctrl_inst_ctx->props_count) {
        ESP_LOGE(TAG, "Max properties limit reached. Cannot add property %s", prop->name);
        return ESP_ERR_NO_MEM;
    }

    uint32_t i = local_ctrl_inst_ctx->props_count;
    local_ctrl_inst_ctx->props[i] = calloc(1, sizeof(esp_local_ctrl_prop_t));
    if (!local_ctrl_inst_ctx->props[i]) {
        ESP_LOGE(TAG, "Failed to allocate memory for new property %s", prop->name);
        return ESP_ERR_NO_MEM;
    }
    local_ctrl_inst_ctx->props[i]->name = strdup(prop->name);
    if (!local_ctrl_inst_ctx->props[i]->name) {
        ESP_LOGE(TAG, "Failed to allocate memory for property data %s", prop->name);
        free(local_ctrl_inst_ctx->props[i]);
        local_ctrl_inst_ctx->props[i] = NULL;
        return ESP_ERR_NO_MEM;
    }
    local_ctrl_inst_ctx->props[i]->type  = prop->type;
    local_ctrl_inst_ctx->props[i]->size  = prop->size;
    local_ctrl_inst_ctx->props[i]->flags = prop->flags;
    local_ctrl_inst_ctx->props[i]->ctx   = prop->ctx;
    local_ctrl_inst_ctx->props[i]->ctx_free_fn = prop->ctx_free_fn;
    local_ctrl_inst_ctx->props_count++;
    return ESP_OK;
}


esp_err_t esp_local_ctrl_remove_property(const char *name)
{
    int idx = esp_local_ctrl_get_property_index(name);
    if (idx < 0) {
        ESP_LOGE(TAG, "Property %s not found", name);
        return ESP_ERR_NOT_FOUND;
    }

    /* Release memory allocated for property data */
    if (local_ctrl_inst_ctx->props[idx]->ctx_free_fn) {
        local_ctrl_inst_ctx->props[idx]->ctx_free_fn(
            local_ctrl_inst_ctx->props[idx]->ctx);
    }
    free(local_ctrl_inst_ctx->props[idx]->name);
    free(local_ctrl_inst_ctx->props[idx]);
    local_ctrl_inst_ctx->props[idx++] = NULL;

    /* Move the following properties forward, so that there is
     * no empty space between two properties */
    for (uint32_t i = idx; i < local_ctrl_inst_ctx->props_count; i++) {
        if (local_ctrl_inst_ctx->props[i] == NULL) {
            break;
        }
        local_ctrl_inst_ctx->props[i-1] = local_ctrl_inst_ctx->props[i];
    }
    local_ctrl_inst_ctx->props_count--;
    return ESP_OK;
}

const esp_local_ctrl_prop_t *esp_local_ctrl_get_property(const char *name)
{
    int idx = esp_local_ctrl_get_property_index(name);
    if (idx < 0) {
        ESP_LOGE(TAG, "Property %s not found", name);
        return NULL;
    }

    return local_ctrl_inst_ctx->props[idx];
}

esp_err_t esp_local_ctrl_get_prop_count(size_t *count)
{
    if (!local_ctrl_inst_ctx) {
        ESP_LOGE(TAG, "Service not running");
        return ESP_ERR_INVALID_STATE;
    }
    if (!count) {
        return ESP_ERR_INVALID_ARG;
    }
    *count = local_ctrl_inst_ctx->props_count;
    return ESP_OK;
}

esp_err_t esp_local_ctrl_get_prop_values(size_t total_indices, uint32_t *indices,
                                         esp_local_ctrl_prop_t *props,
                                         esp_local_ctrl_prop_val_t *values)
{
    if (!local_ctrl_inst_ctx) {
        ESP_LOGE(TAG, "Service not running");
        return ESP_ERR_INVALID_STATE;
    }
    if (!indices || !props || !values) {
        return ESP_ERR_INVALID_ARG;
    }

    /* Convert indices to names */
    for (size_t i = 0; i < total_indices; i++) {
        if (indices[i] >= local_ctrl_inst_ctx->props_count) {
            ESP_LOGE(TAG, "Invalid property index %" PRId32, indices[i]);
            return ESP_ERR_INVALID_ARG;
        }
        props[i].name  = local_ctrl_inst_ctx->props[indices[i]]->name;
        props[i].type  = local_ctrl_inst_ctx->props[indices[i]]->type;
        props[i].flags = local_ctrl_inst_ctx->props[indices[i]]->flags;
        props[i].size  = local_ctrl_inst_ctx->props[indices[i]]->size;
        props[i].ctx   = local_ctrl_inst_ctx->props[indices[i]]->ctx;
    }

    esp_local_ctrl_handlers_t *h = &local_ctrl_inst_ctx->config.handlers;
    esp_err_t ret = h->get_prop_values(total_indices, props, values, h->usr_ctx);

    /* Properties with fixed sizes need to be checked */
    for (size_t i = 0; i < total_indices; i++) {
        if (local_ctrl_inst_ctx->props[indices[i]]->size != 0) {
            values[i].size = local_ctrl_inst_ctx->props[indices[i]]->size;
        }
    }
    return ret;
}

esp_err_t esp_local_ctrl_set_prop_values(size_t total_indices, uint32_t *indices,
                                         const esp_local_ctrl_prop_val_t *values)
{
   if (!local_ctrl_inst_ctx) {
        ESP_LOGE(TAG, "Service not running");
        return ESP_ERR_INVALID_STATE;
    }
    if (!indices || !values) {
        return ESP_ERR_INVALID_ARG;
    }

    esp_local_ctrl_prop_t *props = calloc(total_indices,
                                          sizeof(esp_local_ctrl_prop_t));
    if (!props) {
        ESP_LOGE(TAG, "Unable to allocate memory for properties array");
        return ESP_ERR_NO_MEM;
    }
    for (size_t i = 0; i < total_indices; i++) {
        if (indices[i] >= local_ctrl_inst_ctx->props_count) {
            ESP_LOGE(TAG, "Invalid property index %" PRId32, indices[i]);
            free(props);
            return ESP_ERR_INVALID_ARG;
        }

        /* Properties with fixed sizes need to be checked */
        if ((local_ctrl_inst_ctx->props[indices[i]]->size != values[i].size) &&
            (local_ctrl_inst_ctx->props[indices[i]]->size != 0)) {
            ESP_LOGE(TAG, "Invalid property size %d. Expected %d",
                     values[i].size, local_ctrl_inst_ctx->props[indices[i]]->size);
            free(props);
            return ESP_ERR_INVALID_ARG;
        }

        props[i].name  = local_ctrl_inst_ctx->props[indices[i]]->name;
        props[i].type  = local_ctrl_inst_ctx->props[indices[i]]->type;
        props[i].flags = local_ctrl_inst_ctx->props[indices[i]]->flags;
        props[i].size  = local_ctrl_inst_ctx->props[indices[i]]->size;
        props[i].ctx   = local_ctrl_inst_ctx->props[indices[i]]->ctx;
    }

    esp_local_ctrl_handlers_t *h = &local_ctrl_inst_ctx->config.handlers;
    esp_err_t ret = h->set_prop_values(total_indices, props, values, h->usr_ctx);

    free(props);
    return ret;
}

esp_err_t esp_local_ctrl_set_handler(const char *ep_name,
                                     protocomm_req_handler_t handler,
                                     void *priv_data)
{
    esp_err_t ret = ESP_ERR_INVALID_STATE;

    if (local_ctrl_inst_ctx) {
        ret = protocomm_add_endpoint(local_ctrl_inst_ctx->pc, ep_name,
                                     handler, priv_data);
    }

    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Failed to register endpoint handler");
    }
    return ret;
}
