// Copyright 2018 Espressif Systems (Shanghai) PTE LTD
//
// 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.

#include <stdio.h>
#include <string.h>
#include <esp_log.h>
#include <esp_console.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/queue.h>
#include <driver/uart.h>

#include <protocomm.h>
#include <protocomm_console.h>

#include "protocomm_priv.h"

static const char *TAG = "protocomm_console";

static uint32_t session_id = PROTOCOMM_NO_SESSION_ID;
static protocomm_t *pc_console   = NULL; /* The global protocomm instance for console */
static TaskHandle_t console_task = NULL;

esp_err_t protocomm_console_stop(protocomm_t *pc)
{
    if (pc != pc_console) {
        ESP_LOGE(TAG, "Incorrect stop request");
        return ESP_ERR_INVALID_ARG;
    }

    ESP_LOGI(TAG, "Stopping console...");
    xTaskNotifyGive(console_task);
    return ESP_OK;
}

static ssize_t hex2bin(const char *hexstr, uint8_t *bytes)
{
    size_t hexstrLen = strlen(hexstr);
    ssize_t bytesLen = hexstrLen / 2;

    int count = 0;
    const char *pos = hexstr;

    for(count = 0; count < bytesLen; count++) {
        sscanf(pos, "%2hhx", &bytes[count]);
        pos += 2;
    }

    return bytesLen;
}

static bool stopped(void)
{
    uint32_t flag = 0;
    xTaskNotifyWait(0, 0, &flag, (TickType_t) 10/portTICK_RATE_MS);
    return (flag != 0);
}

static void protocomm_console_task(void *arg)
{
    int uart_num = (int) arg;
    uint8_t linebuf[256];
    int i, cmd_ret;
    esp_err_t ret;
    QueueHandle_t uart_queue;
    uart_event_t event;

    ESP_LOGV(TAG, "Initialising UART on port %d", uart_num);
    uart_driver_install(uart_num, 256, 0, 8, &uart_queue, 0);
    /* Initialize the console */
    esp_console_config_t console_config = {
            .max_cmdline_args = 8,
            .max_cmdline_length = 256,
    };

    esp_console_init(&console_config);
    esp_console_register_help_command();

    while (!stopped()) {
        uart_write_bytes(uart_num, "\n>> ", 4);
        memset(linebuf, 0, sizeof(linebuf));
        i = 0;
        do {
            ret = xQueueReceive(uart_queue, (void * )&event, (TickType_t) 10/portTICK_RATE_MS);
            if (ret != pdPASS) {
                if (stopped()) {
                    break;
                } else {
                    continue;
                }
            }
            if (event.type == UART_DATA) {
                while (uart_read_bytes(uart_num, (uint8_t *) &linebuf[i], 1, 0)) {
                    if (linebuf[i] == '\r') {
                        uart_write_bytes(uart_num, "\r\n", 2);
                    } else {
                        uart_write_bytes(uart_num, (char *) &linebuf[i], 1);
                    }
                    i++;
                }
            }
        } while ((i < 255) && linebuf[i-1] != '\r');
        if (stopped()) {
            break;
        }
        ret = esp_console_run((char *) linebuf, &cmd_ret);
        if (ret < 0) {
            ESP_LOGE(TAG, "Console dispatcher error\n");
            break;
        }
    }

    if (pc_console->sec && pc_console->sec->cleanup) {
        pc_console->sec->cleanup();
    }

    pc_console = NULL;
    esp_console_deinit();

    ESP_LOGI(TAG, "Console stopped");
    vTaskDelete(NULL);
}

static int common_cmd_handler(int argc, char** argv)
{
    if (argc < 3) {
        return ESP_ERR_INVALID_ARG;
    }

    int i, ret;

    uint32_t cur_session_id = atoi(argv[1]);

    uint8_t *buf = (uint8_t *) malloc(strlen(argv[2]));
    uint8_t *outbuf;
    ssize_t outlen;
    ssize_t len = hex2bin(argv[2], buf);

    if (cur_session_id != session_id) {
        if (pc_console->sec && pc_console->sec->new_transport_session) {
            ret = pc_console->sec->new_transport_session(cur_session_id);
            if (ret == ESP_OK) {
                session_id = cur_session_id;
            }
        }
    }

    ret = protocomm_req_handle(pc_console, argv[0], cur_session_id, buf, len, &outbuf, &outlen);
    free(buf);

    if (ret == ESP_OK) {
        printf("\r\n");
        for (i = 0; i < outlen; i++) {
            printf("%02x", outbuf[i]);
        }
        printf("\r\n");

        /* Transport is responsible for freeing the transmit buffer */
        free(outbuf);

        return ESP_OK;
    } else {
        return ret;
    }
}

static esp_err_t protocomm_console_add_endpoint(const char *ep_name, protocomm_req_handler_t req_handler, void *priv_data)
{
    (void) req_handler;
    (void) priv_data;

    esp_err_t ret;
    esp_console_cmd_t cmd;
    memset(&cmd, 0, sizeof(cmd));

    cmd.command = ep_name;
    cmd.help = "";
    cmd.func = common_cmd_handler;

    ret = esp_console_cmd_register(&cmd);

    return ret;
}

static esp_err_t protocomm_console_remove_endpoint(const char *ep_name)
{
    /* Command deletion happens internally in esp_console_deinit function */

    return ESP_OK;
}

esp_err_t protocomm_console_start(protocomm_t *pc, const protocomm_console_config_t *config)
{
    if (pc == NULL) {
        return ESP_ERR_INVALID_ARG;
    }

    if (pc_console != NULL) {
        if (pc_console == pc) {
            return ESP_ERR_INVALID_STATE;
        }
        else {
            return ESP_ERR_NOT_SUPPORTED;
        }
    }


    if (xTaskCreate(protocomm_console_task, "protocomm_console",
                    config->stack_size, NULL, config->task_priority, &console_task) != pdPASS) {
        return ESP_FAIL;
    }

    pc->add_endpoint = protocomm_console_add_endpoint;
    pc->remove_endpoint = protocomm_console_remove_endpoint;
    pc_console = pc;
    return ESP_OK;
}
