// Copyright 2015-2019 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 <string.h>
#include "esp_gdbstub.h"
#include "esp_gdbstub_common.h"
#include "sdkconfig.h"


#ifdef CONFIG_ESP_GDBSTUB_SUPPORT_TASKS
static void init_task_info(void);
static void find_paniced_task_index(void);
static int handle_task_commands(unsigned char *cmd, int len);
static void esp_gdbstub_send_str_as_hex(const char *str);
#endif

static void send_reason(void);


static esp_gdbstub_scratch_t s_scratch;


void esp_gdbstub_panic_handler(esp_gdbstub_frame_t *frame)
{
#ifndef CONFIG_ESP_GDBSTUB_SUPPORT_TASKS
    esp_gdbstub_frame_to_regfile(frame, &s_scratch.regfile);
#else
    if (s_scratch.state == GDBSTUB_STARTED) {
        /* We have re-entered GDB Stub. Try disabling task support. */
        s_scratch.state = GDBSTUB_TASK_SUPPORT_DISABLED;
        /* Flush any pending GDB packet (this creates a garbage value) */
        esp_gdbstub_send_end();
    } else if (s_scratch.state == GDBSTUB_NOT_STARTED) {
        s_scratch.state = GDBSTUB_STARTED;
        /* Save the paniced frame and get the list of tasks */
        memcpy(&s_scratch.paniced_frame, frame, sizeof(*frame));
        esp_gdbstub_frame_to_regfile(frame, &s_scratch.regfile);
        init_task_info();
        find_paniced_task_index();
        /* Current task is the paniced task */
        if (s_scratch.paniced_task_index == GDBSTUB_CUR_TASK_INDEX_UNKNOWN) {
            s_scratch.current_task_index = 0;
        }
    }
#endif // CONFIG_ESP_GDBSTUB_SUPPORT_TASKS

    esp_gdbstub_target_init();
    s_scratch.signal = esp_gdbstub_get_signal(frame);
    send_reason();
    while (true) {
        unsigned char *cmd;
        size_t size;
        int res = esp_gdbstub_read_command(&cmd, &size);
        if (res > 0) {
            /* character received instead of a command */
            continue;
        }
        if (res == GDBSTUB_ST_ERR) {
            esp_gdbstub_send_str_packet("E01");
            continue;
        }
        res = esp_gdbstub_handle_command(cmd, size);
        if (res == GDBSTUB_ST_ERR) {
            esp_gdbstub_send_str_packet(NULL);
        }
    }
}

static void send_reason(void)
{
    esp_gdbstub_send_start();
    esp_gdbstub_send_char('T');
    esp_gdbstub_send_hex(s_scratch.signal, 8);
    esp_gdbstub_send_end();
}

static uint32_t gdbstub_hton(uint32_t i)
{
    return __builtin_bswap32(i);
}

#ifdef CONFIG_ESP_GDBSTUB_SUPPORT_TASKS
static void esp_gdbstub_send_str_as_hex(const char *str)
{
    while (*str) {
        esp_gdbstub_send_hex(*str, 8);
        str++;
    }    
}
#endif

/** Send all registers to gdb */
static void handle_g_command(const unsigned char* cmd, int len)
{
    uint32_t *p = (uint32_t *) &s_scratch.regfile;
    esp_gdbstub_send_start();
    for (int i = 0; i < sizeof(s_scratch.regfile) / sizeof(*p); ++i) {
        esp_gdbstub_send_hex(gdbstub_hton(*p++), 32);
    }
    esp_gdbstub_send_end();
}

/** Receive register values from gdb */
static void handle_G_command(const unsigned char* cmd, int len)
{
    uint32_t *p = (uint32_t *) &s_scratch.regfile;
    for (int i = 0; i < sizeof(s_scratch.regfile) / sizeof(*p); ++i) {
        *p++ = gdbstub_hton(esp_gdbstub_gethex(&cmd, 32));
    }
    esp_gdbstub_send_str_packet("OK");
}

/** Read memory to gdb */
static void handle_m_command(const unsigned char* cmd, int len)
{
    intptr_t addr = (intptr_t) esp_gdbstub_gethex(&cmd, -1);
    cmd++;
    uint32_t size = esp_gdbstub_gethex(&cmd, -1);

    if (esp_gdbstub_readmem(addr) < 0 || esp_gdbstub_readmem(addr + size - 1) < 0) {
        esp_gdbstub_send_str_packet("E01");
        return;
    }

    esp_gdbstub_send_start();
    for (int i = 0; i < size; ++i) {
        int b = esp_gdbstub_readmem(addr++);
        esp_gdbstub_send_hex(b, 8);
    }
    esp_gdbstub_send_end();
}

/** Handle a command received from gdb */
int esp_gdbstub_handle_command(unsigned char *cmd, int len)
{
    unsigned char *data = cmd + 1;
    if (cmd[0] == 'g') 
    {
        handle_g_command(data, len - 1);
    } else if (cmd[0] == 'G') {
        /* receive content for all registers from gdb */
        handle_G_command(data, len - 1);
    } else if (cmd[0] == 'm') {
        /* read memory to gdb */
        handle_m_command(data, len - 1);
    } else if (cmd[0] == '?') {
        /* Reply with stop reason */
        send_reason();
#if CONFIG_ESP_GDBSTUB_SUPPORT_TASKS
    } else if (s_scratch.state != GDBSTUB_TASK_SUPPORT_DISABLED) {
        return handle_task_commands(cmd, len);
#endif // CONFIG_ESP_GDBSTUB_SUPPORT_TASKS
    } else {
        /* Unrecognized command */
        return GDBSTUB_ST_ERR;
    }
    return GDBSTUB_ST_OK;
}

/* Everything below is related to the support for listing FreeRTOS tasks as threads in GDB */

#ifdef CONFIG_ESP_GDBSTUB_SUPPORT_TASKS

static void init_task_info(void)
{
    unsigned tcb_size;
    s_scratch.task_count = uxTaskGetSnapshotAll(s_scratch.tasks, GDBSTUB_TASKS_NUM, &tcb_size);
}

static bool get_task_handle(size_t index, TaskHandle_t *handle)
{
    if (index >= s_scratch.task_count) {
        return false;
    }
    *handle = (TaskHandle_t) s_scratch.tasks[index].pxTCB;
    return true;
}

static eTaskState get_task_state(size_t index) 
{
    return s_scratch.tasks[index].eState;
}

static int get_task_cpu_id(size_t index) 
{
    return s_scratch.tasks[index].xCpuId;
}

/** Get the index of the task running on the current CPU, and save the result */
static void find_paniced_task_index(void)
{
    TaskHandle_t cur_handle = xTaskGetCurrentTaskHandleForCPU(xPortGetCoreID());
    TaskHandle_t handle;
    for (int i = 0; i < s_scratch.task_count; i++) {
        if (get_task_handle(i, &handle) && cur_handle == handle) {
            s_scratch.paniced_task_index = i;
            return;
        }
    }
    s_scratch.paniced_task_index = GDBSTUB_CUR_TASK_INDEX_UNKNOWN;
}

/** H command sets the "current task" for the purpose of further commands */
static void handle_H_command(const unsigned char* cmd, int len)
{
    if (cmd[1] == 'g' || cmd[1] == 'c') {
        const char *ret = "OK";
        cmd += 2;
        int requested_task_index = esp_gdbstub_gethex(&cmd, -1);
        if (requested_task_index == s_scratch.paniced_task_index ||
                (requested_task_index == 0 && s_scratch.current_task_index == GDBSTUB_CUR_TASK_INDEX_UNKNOWN)) {
            /* Get the registers of the paniced task */
            esp_gdbstub_frame_to_regfile(&s_scratch.paniced_frame, &s_scratch.regfile);
        } else if (requested_task_index > s_scratch.task_count) {
            ret = "E00";
        } else {
            TaskHandle_t handle = NULL;
            get_task_handle(requested_task_index, &handle);
            /* FIXME: for the task currently running on the other CPU, extracting the registers from TCB
             * isn't valid. Need to use some IPC mechanism to obtain the registers of the other CPU
             */
            if (handle != NULL) {
                esp_gdbstub_tcb_to_regfile(handle, &s_scratch.regfile);
            }
        }
        esp_gdbstub_send_str_packet(ret);
    } else {
        esp_gdbstub_send_str_packet(NULL);
    }
}

/** qC returns the current thread ID */
static void handle_qC_command(const unsigned char* cmd, int len)
{
    esp_gdbstub_send_start();
    esp_gdbstub_send_str("QC");
    esp_gdbstub_send_hex(s_scratch.current_task_index, 32);
    esp_gdbstub_send_end();
}

/** T command checks if the task is alive.
 *  Since GDB isn't going to ask about the tasks which haven't been listed by q*ThreadInfo,
 *  and the state of tasks can not change (no stepping allowed), simply return "OK" here.
 */
static void handle_T_command(const unsigned char* cmd, int len)
{
    esp_gdbstub_send_str_packet("OK");
}

/** qfThreadInfo requests the start of the thread list, qsThreadInfo (below) is repeated to
 *  get the subsequent threads.
 */
static void handle_qfThreadInfo_command(const unsigned char* cmd, int len)
{
    /* The first task in qfThreadInfo reply is going to be the one which GDB will request to stop.
     * Therefore it has to be the paniced task.
     * Reply with the paniced task index, and later skip over this index while handling qsThreadInfo
     */
    esp_gdbstub_send_start();
    esp_gdbstub_send_str("m");
    esp_gdbstub_send_hex(s_scratch.paniced_task_index, 32);
    esp_gdbstub_send_end();

    s_scratch.thread_info_index = 0;
}

static void handle_qsThreadInfo_command(const unsigned char* cmd, int len)
{
    int next_task_index = ++s_scratch.thread_info_index;

    if (next_task_index == s_scratch.task_count) {
        /* No more tasks */
        esp_gdbstub_send_str_packet("l");
        return;
    }

    if (next_task_index == s_scratch.paniced_task_index) {
        /* Have already sent this one in the reply to qfThreadInfo, skip over it */
        handle_qsThreadInfo_command(cmd, len);
        return;
    }

    esp_gdbstub_send_start();
    esp_gdbstub_send_str("m");
    esp_gdbstub_send_hex(next_task_index, 32);
    esp_gdbstub_send_end();
}

/** qThreadExtraInfo requests the thread name */
static void handle_qThreadExtraInfo_command(const unsigned char* cmd, int len)
{ 
    cmd += sizeof("qThreadExtraInfo,") - 1;
    int task_index = esp_gdbstub_gethex(&cmd, -1);
    TaskHandle_t handle;
    if (!get_task_handle(task_index, &handle)) {
        esp_gdbstub_send_str_packet("E01");
        return;
    }
    esp_gdbstub_send_start();
    esp_gdbstub_send_str_as_hex("Name: ");
    esp_gdbstub_send_str_as_hex(pcTaskGetTaskName(handle));
    esp_gdbstub_send_hex(' ', 8);
    
    eTaskState state = get_task_state(task_index);
    switch (state) {
        case eRunning:
            esp_gdbstub_send_str_as_hex("State: Running ");
            esp_gdbstub_send_str_as_hex("@CPU");
            esp_gdbstub_send_hex(get_task_cpu_id(task_index) + '0', 8);
        break;
        case eReady:
            esp_gdbstub_send_str_as_hex("State: Ready"); 
        break;
        case eBlocked:
            esp_gdbstub_send_str_as_hex("State: Blocked"); 
        break;
        case eSuspended:
            esp_gdbstub_send_str_as_hex("State: Suspended"); 
        break;
        case eDeleted:
            esp_gdbstub_send_str_as_hex("State: Deleted"); 
        break;
        default:
            esp_gdbstub_send_str_as_hex("State: Invalid"); 
        break;
    }
    
    esp_gdbstub_send_end();
}

bool command_name_matches(const char* pattern, const unsigned char* ucmd, int len)
{
    const char* cmd = (const char*) ucmd;
    const char* end = cmd + len;
    for (; *pattern && cmd < end; ++cmd, ++pattern) {
        if (*pattern == '?') {
            continue;
        }
        if (*pattern != *cmd) {
            return false;
        }
    }
    return *pattern == 0 && (cmd == end || *cmd == ',');
}

/** Handle all the thread-related commands */
static int handle_task_commands(unsigned char *cmd, int len)
{
    if (cmd[0] == 'H') {
        /* Continue with task */
        handle_H_command(cmd, len);
    } else if (cmd[0] == 'T') {
        /* Task alive check */
        handle_T_command(cmd, len);
    } else if (cmd[0] == 'q') {
        if (command_name_matches("qfThreadInfo", cmd, len)) {
            handle_qfThreadInfo_command(cmd, len);
        } else if (command_name_matches("qsThreadInfo", cmd, len)) {
            handle_qsThreadInfo_command(cmd, len);
        } else if (command_name_matches("qC", cmd, len)) {
            handle_qC_command(cmd, len);
        } else if (command_name_matches("qThreadExtraInfo", cmd, len)) {
            handle_qThreadExtraInfo_command(cmd, len);
        } else {
            /* Unrecognized command */
            return GDBSTUB_ST_ERR;
        }
    } else {
        /* Unrecognized command */
        return GDBSTUB_ST_ERR;
    }
    return GDBSTUB_ST_OK;
}

#endif // CONFIG_ESP_GDBSTUB_SUPPORT_TASKS