/** @file command.c
 *
 *  A versatile software tool to analyse and configure CANopen devices.
 *
 *  Copyright (c) 2026, Michael Fitzmayer. All rights reserved.
 *  SPDX-License-Identifier: MIT
 *
 **/

#include <stdlib.h>

#include "can.h"
#include "command.h"
#include "core.h"
#include "dict.h"
#include "eds.h"
#include "nmt.h"
#include "os.h"
#include "pdo.h"
#include "scripts.h"
#include "sdo.h"
#include "table.h"

static void convert_token_to_uint(char* token, uint32* result);
static void convert_token_to_uint64(char* token, uint64* result);
static bool has_node_id_arg(const char* prefix, char cmd);
status_t print_usage_information(bool show_all);
static bool is_numeric(const char* str);

void parse_command(char* input, core_t* core)
{
    int index;
    char* delim = " \n";
    char* context;
    char* token = NULL;
    char* input_savptr = input;
    uint32 command;

    token = os_strtokr_r(input_savptr, delim, &input_savptr);

    if (NULL == token)
    {
        return;
    }
    else if ((input[1] != ' ') && (input[1] != '\0'))
    {
        run_script(token, core);
        return;
    }
    else if (0 == os_strncmp(token, "y", 1))
    {
        uint32 channel_index;

        token = os_strtokr_r(input_savptr, delim, &input_savptr);
        if (NULL == token)
        {
            can_print_channel_help(core);
            return;
        }
        else
        {
            convert_token_to_uint(token, &channel_index);
        }

        can_set_channel(channel_index, core);
    }
    else if (0 == os_strncmp(token, "b", 1))
    {
        uint32 baud_rate_index;

        token = os_strtokr_r(input_savptr, delim, &input_savptr);
        if (NULL == token)
        {
            can_print_baud_rate_help(core);
            return;
        }
        else
        {
            convert_token_to_uint(token, &baud_rate_index);
        }

        can_set_baud_rate(baud_rate_index, core);
    }
    else if (0 == os_strncmp(token, "c", 1))
    {
        if (0 != system(CLEAR_CMD))
        {
            os_log(LOG_WARNING, "Could not clear screen");
        }
    }
    else if (0 == os_strncmp(token, "d", 1))
    {
        uint32 file_no;
        uint32 sub_index;

        token = os_strtokr_r(input_savptr, delim, &input_savptr);
        if (NULL == token)
        {
            list_codb();
            return;
        }

        convert_token_to_uint(token, &file_no);

        token = os_strtokr_r(input_savptr, delim, &input_savptr);
        if (NULL == token)
        {
            load_codb(file_no);
        }
        else
        {
            convert_token_to_uint(token, &sub_index);
            (void)dict_lookup_object(file_no, sub_index);
        }
    }
    else if (0 == os_strncmp(token, "q", 1))
    {
        core->is_running = false;
    }
    else if (0 == os_strncmp(token, "h", 1))
    {
        print_usage_information(true);
    }
    else if (0 == os_strncmp(token, "n", 1))
    {
        uint32 node_id;
        uint32 command;
        size_t token_len;

        token = os_strtokr_r(input_savptr, delim, &input_savptr);
        if (NULL == token)
        {
            print_usage_information(false);
            return;
        }

        convert_token_to_uint(token, &node_id);

        token = os_strtokr_r(input_savptr, delim, &input_savptr);
        if (NULL == token)
        {
            nmt_print_help(TERM_MODE);
            return;
        }

        token_len = os_strlen(token);

        if (0 == os_strncmp(token, "op", token_len))
        {
            command = NMT_OPERATIONAL;
        }
        else if (0 == os_strncmp(token, "stop", token_len))
        {
            command = NMT_STOP;
        }
        else if (0 == os_strncmp(token, "preop", token_len))
        {
            command = NMT_PRE_OPERATIONAL;
        }
        else if (0 == os_strncmp(token, "reset", token_len))
        {
            command = NMT_RESET_NODE;
        }
        else if (0 == os_strncmp(token, "comm", token_len))
        {
            command = NMT_RESET_COMM;
        }
        else
        {
            convert_token_to_uint(token, &command);
        }

        nmt_send_command((uint16)node_id, (uint8)command, TERM_MODE, NULL);
    }
    else if (0 == os_strncmp(token, "l", 1))
    {
        list_scripts();
    }
    else if (0 == os_strncmp(token, "p", 1))
    {
        uint32 can_id;

        token = os_strtokr_r(input_savptr, delim, &input_savptr);
        if (NULL == token)
        {
            print_usage_information(false);
            return;
        }
        else if (0 == os_strncmp(token, "add", 3))
        {
            uint32 event_time_ms;
            uint32 length;
            uint64 data;

            token = os_strtokr_r(input_savptr, delim, &input_savptr);
            if (NULL == token)
            {
                print_usage_information(false);
                return;
            }

            convert_token_to_uint(token, &can_id);

            token = os_strtokr_r(input_savptr, delim, &input_savptr);
            if (NULL == token)
            {
                print_usage_information(false);
                return;
            }

            convert_token_to_uint(token, &event_time_ms);

            token = os_strtokr_r(input_savptr, delim, &input_savptr);
            if (NULL == token)
            {
                print_usage_information(false);
                return;
            }

            convert_token_to_uint(token, &length);

            token = os_strtokr_r(input_savptr, delim, &input_savptr);
            if (NULL == token)
            {
                print_usage_information(false);
                return;
            }

            convert_token_to_uint64(token, &data);

            if (false == pdo_is_id_valid(can_id))
            {
                pdo_print_help();
                return;
            }

            if (false == is_can_initialised(core))
            {
                os_log(LOG_WARNING, "Could not add PDO: CAN not initialised");
                return;
            }

            pdo_add((uint16)can_id, event_time_ms, length, data, TERM_MODE);
        }
        else if (0 == os_strncmp(token, "del", 3))
        {
            token = os_strtokr_r(input_savptr, delim, &input_savptr);
            if (NULL == token)
            {
                print_usage_information(false);
                return;
            }

            convert_token_to_uint(token, &can_id);

            if (false == pdo_is_id_valid(can_id))
            {
                pdo_print_help();
                return;
            }

            if (false == is_can_initialised(core))
            {
                os_log(LOG_WARNING, "Could not delete PDO: CAN not initialised");
                return;
            }

            pdo_del((uint16)can_id, TERM_MODE);
        }
        else
        {
            print_usage_information(false);
            return;
        }
    }
    else if (0 == os_strncmp(token, "r", 1))
    {
        can_message_t sdo_response = {0};
        uint32 node_id;
        uint32 sdo_index;
        uint32 sub_index = 0;

        token = os_strtokr_r(input_savptr, delim, &input_savptr);
        if (NULL == token)
        {
            print_usage_information(false);
            return;
        }

        convert_token_to_uint(token, &node_id);

        token = os_strtokr_r(input_savptr, delim, &input_savptr);
        if (NULL == token)
        {
            print_usage_information(false);
            return;
        }

        convert_token_to_uint(token, &sdo_index);

        token = os_strtokr_r(input_savptr, delim, &input_savptr);
        if (NULL != token)
        {
            convert_token_to_uint(token, &sub_index);
        }

        sdo_read(&sdo_response, TERM_MODE, node_id, sdo_index, sub_index, NULL);
    }
    else if (0 == os_strncmp(token, "w", 1))
    {
        can_message_t sdo_response = {0};
        uint32 node_id;
        uint32 sdo_index;
        uint32 sub_index;
        uint32 sdo_data_length = 0;
        uint32 sdo_data = 0;
        sdo_state_t sdo_state = IS_WRITE_EXPEDITED;

        token = os_strtokr_r(input_savptr, delim, &input_savptr);
        if (token == NULL)
        {
            print_usage_information(false);
            return;
        }
        convert_token_to_uint(token, &node_id);

        token = os_strtokr_r(input_savptr, delim, &input_savptr);
        if (token == NULL)
        {
            print_usage_information(false);
            return;
        }
        convert_token_to_uint(token, &sdo_index);

        token = os_strtokr_r(input_savptr, delim, &input_savptr);
        if (token == NULL)
        {
            print_usage_information(false);
            return;
        }
        convert_token_to_uint(token, &sub_index);

        token = os_strtokr_r(input_savptr, delim, &input_savptr);
        if (token != NULL)
        {
            char buffer[256] = {0};

            if (is_numeric(token))
            {
                convert_token_to_uint(token, &sdo_data_length);

                token = os_strtokr_r(input_savptr, delim, &input_savptr);
                if (token != NULL)
                {
                    convert_token_to_uint(token, &sdo_data);
                }

                os_memcpy(buffer, &sdo_data, sizeof(uint32));
            }
            else
            {
                size_t len;

                os_strlcpy(buffer, token, sizeof(buffer));
                len = os_strlen(buffer);
                sdo_state = IS_WRITE_SEGMENTED;
                token = os_strtokr_r(NULL, delim, &input_savptr);

                while (token != NULL)
                {
                    os_strlcat(buffer, " ", sizeof(buffer));
                    os_strlcat(buffer, token, sizeof(buffer));
                    len = os_strlen(buffer);
                    token = os_strtokr_r(NULL, delim, &input_savptr);
                }

                if (buffer[0] == '"' && buffer[len - 1] == '"')
                {
                    buffer[len - 1] = '\0';
                    os_memmove(buffer, buffer + 1, len - 1);
                }

                sdo_data_length = os_strlen(buffer);
            }

            if (sdo_data_length > 0)
            {
                if (IS_WRITE_EXPEDITED == sdo_state)
                {
                    sdo_write(&sdo_response, TERM_MODE, node_id, sdo_index, sub_index, sdo_data_length, (void*)buffer, NULL);
                }
                else
                {
                    sdo_write_segmented(&sdo_response, TERM_MODE, node_id, sdo_index, sub_index, sdo_data_length, (void*)buffer, NULL);
                }
            }
            else
            {
                print_usage_information(false);
                return;
            }
        }
        else
        {
            print_usage_information(false);
            return;
        }
    }
    else if (0 == os_strncmp(token, "s", 1))
    {
        token = os_strtokr_r(input_savptr, delim, &input_savptr);
        if (NULL == token)
        {
            print_usage_information(true);
            return;
        }

        run_script(token, core);
    }
    else
    {
        print_usage_information(false);
    }
}

static void convert_token_to_uint(char* token, uint32* result)
{
    if (('0' == token[0]) && ('x' == token[1]))
    {
        *result = (uint32)os_strtol(token, NULL, 16);
    }
    else
    {
        *result = (uint32)os_strtol(token, NULL, 10);
    }
}

static void convert_token_to_uint64(char* token, uint64* result)
{
    if (('0' == token[0]) && ('x' == token[1]))
    {
        *result = os_strtoull(token, NULL, 16);
    }
    else
    {
        *result = os_strtoull(token, NULL, 10);
    }
}

status_t print_usage_information(bool show_all)
{
    status_t status;
    table_t table = {DARK_CYAN, DARK_WHITE, 3, 45, 17};

    status = table_init(&table, 1024);
    table_print_header(&table);
    table_print_row("CMD", "Parameter(s)", "Function", &table);
    table_print_divider(&table);
    table_print_row(" h ", " ", "Show full help", &table);

    if (true == show_all)
    {
        table_print_row(" b ", "(identifer)", "Set baud rate", &table);
        table_print_row(" d ", "[file_no]", "Load data base", &table);
        table_print_row(" d ", "[index] [sub_index]", "Lookup dictionary", &table);
        table_print_row(" y ", "(identifer)", "Set CAN channel", &table);
        table_print_row(" c ", " ", "Clear output", &table);
        table_print_row(" l ", " ", "List scripts", &table);
        table_print_row(" s ", "[identifier](.lua)", "Run script", &table);
    }

    table_print_row(" n ", "[node_id] [command or alias]", "NMT command", &table);
    table_print_row(" r ", "[node_id] [index] (sub_index)", "Read SDO", &table);
    table_print_row(" w ", "[node_id] [index] [sub_index] [length] (data)", "Write SDO", &table);
    table_print_row(" w ", "[node_id] [index] [sub_index] [\"data\"]", "Write SDO", &table);
    table_print_row(" p ", "add [can_id] [event_time_ms] [length] [data]", "Add PDO (tx)", &table);
    table_print_row(" p ", "del [can_id]", "Remove PDO (tx)", &table);
    table_print_row(" q ", " ", "Quit", &table);
    table_print_footer(&table);
    table_flush(&table);

    return status;
}

static bool is_numeric(const char* str)
{
    if ((NULL == str) || ('\0' == *str))
    {
        return false;
    }

    while (*str)
    {
        if (0 == os_isdigit(*str))
        {
            return false;
        }
        str += 1;
    }

    return true;
}

static bool has_node_id_arg(const char* prefix, char cmd)
{
    if (prefix[0] != cmd || prefix[1] != ' ')
    {
        return false;
    }

    const char* p = prefix + 2;

    if (p[0] == '0' && (p[1] == 'x' || p[1] == 'X'))
    {
        p += 2;
        if (0 == os_isxdigit(*p))
        {
            return false;
        }
        while (os_isxdigit(*p))
        {
            p++;
        }
    }
    else if (os_isdigit(*p))
    {
        while (os_isdigit(*p))
        {
            p++;
        }
    }
    else
    {
        return false;
    }

    return *p == ' ';
}

static const char* s_completion_prefix = NULL;

static void completion_add(completions_t* cenv, const char* word)
{
    const char* prefix = s_completion_prefix;
    (void)word;

    if (NULL == prefix)
    {
        return;
    }

    if (prefix[0] == '\0')
    {
        // Empty line TAB -> suggest commands.
        os_completion_add(cenv, "b", "b", "Set baud rate");
        os_completion_add(cenv, "d", "d", "Load data base");
        os_completion_add(cenv, "n", "n", "NMT command");
        os_completion_add(cenv, "q", "q", "Quit");
        os_completion_add(cenv, "r", "r", "Read SDO");
        os_completion_add(cenv, "s", "s", "Run script");
        os_completion_add(cenv, "w", "w", "Write SDO");
    }
    else if (prefix[0] == 'b' && prefix[1] == ' ')
    {
        os_completion_add(cenv, "1", "1 (1 MBit/s)", "1 MBit/s");
        os_completion_add(cenv, "2", "2 (800 kBit/s)", "800 kBit/s");
        os_completion_add(cenv, "3", "3 (500 kBit/s)", "500 kBit/s");
        os_completion_add(cenv, "4", "4 (250 kBit/s)", "250 kBit/s");
        os_completion_add(cenv, "5", "5 (125 kBit/s)", "125 kBit/s");
        os_completion_add(cenv, "6", "6 (100 kBit/s)", "100 kBit/s");
        os_completion_add(cenv, "7", "7 (95.238 kBit/s)", "95.238 kBit/s");
        os_completion_add(cenv, "8", "8 (83.333 kBit/s)", "83.333 kBit/s");
        os_completion_add(cenv, "9", "9 (50 kBit/s)", "50 kBit/s");
        os_completion_add(cenv, "10", "10 (47.619 kBit/s)", "47.619 kBit/s");
        os_completion_add(cenv, "11", "11 (33.333 kBit/s)", "33.333 kBit/s");
        os_completion_add(cenv, "12", "12 (20 kBit/s)", "20 kBit/s");
        os_completion_add(cenv, "13", "13 (10 kBit/s)", "10 kBit/s");
        os_completion_add(cenv, "14", "14 (5 kBit/s)", "5 kBit/s");
    }
    else if (prefix[0] == 'd' && prefix[1] == ' ')
    {
        os_completion_add(cenv, "1", "1 (CiA 301)", "CiA 301 Application layer and communication");
        os_completion_add(cenv, "2", "2 (CiA 302)", "CiA 302 Programmable CANopen Devices");
        os_completion_add(cenv, "3", "3 (CiA 401)", "CiA 401 I/O devices");
        os_completion_add(cenv, "4", "4 (CiA 402)", "CiA 402 Drives and motion control");
        os_completion_add(cenv, "5", "5 (CiA 404)", "CiA 404 Measurement Devices and Closed Loop Controllers");
        os_completion_add(cenv, "6", "6 (CiA 406)", "CiA 406 Encoders");
        os_completion_add(cenv, "7", "7 (CiA 408)", "CiA 408 Fluid Power Technology");
        os_completion_add(cenv, "8", "8 (CiA 410)", "CiA 410 Inclinometer");
        os_completion_add(cenv, "9", "9 (CiA 413)", "CiA 413 Truck Gateways");
        os_completion_add(cenv, "10", "10 (CiA 415)", "CiA 415 Road construction machinery");
        os_completion_add(cenv, "11", "11 (CiA 416)", "CiA 416 Building door control");
        os_completion_add(cenv, "12", "12 (CiA 417)", "CiA 417 Lift control systems");
        os_completion_add(cenv, "13", "13 (CiA 418)", "CiA 418 Battery modules");
        os_completion_add(cenv, "14", "14 (CiA 419)", "CiA 419 Battery chargers");
        os_completion_add(cenv, "15", "15 (CiA 443)", "CiA 443 SIIS level-2 devices");
        os_completion_add(cenv, "16", "16 (CiA 447)", "CiA 447 Special-purpose car add-on devices");
    }
    else if (has_node_id_arg(prefix, 'n'))
    {
        os_completion_add(cenv, "op", "op", "Start (go to Operational)");
        os_completion_add(cenv, "stop", "stop", "Stop (go to Stopped)");
        os_completion_add(cenv, "preop", "preop", "Go to Pre-operational");
        os_completion_add(cenv, "reset", "reset", "Reset node (Application reset)");
        os_completion_add(cenv, "comm", "comm", "Reset communication");
    }
    else if (has_node_id_arg(prefix, 'r'))
    {
        os_completion_add(cenv, "0x1000", "1000h", "Read Device Type");
        os_completion_add(cenv, "0x1001", "1001h", "Read Error Register");
        os_completion_add(cenv, "0x1002", "1002h", "Read Manufacturer Status Register");
        os_completion_add(cenv, "0x1003", "1003h", "Read Pre-defined Error Field");
        os_completion_add(cenv, "0x1008", "1008h", "Read Manufacturer Device Name");
        os_completion_add(cenv, "0x1009", "1009h", "Read Manufacturer Hardware Version");
        os_completion_add(cenv, "0x100a", "100Ah", "Read Manufacturer Software Version");
        os_completion_add(cenv, "0x1018", "1018h", "Read Identity Object");
    }
    else if (has_node_id_arg(prefix, 'w'))
    {
        os_completion_add(cenv, "0x1010", "1010h", "Write Store Parameters");
        os_completion_add(cenv, "0x1011", "1011h", "Write Restore Default Parameters");
        os_completion_add(cenv, "0x1016", "1016h", "Write Consumer Heartbeat Time");
        os_completion_add(cenv, "0x1017", "1017h", "Write Producer Heartbeat Time");
    }
    else if ((prefix[0] == 's' && prefix[1] == ' ') || (prefix[0] == ' '))
    {
        scripts_add_completions(cenv);
    }
}

void completion_callback(completions_t* cenv, const char* prefix)
{
    s_completion_prefix = prefix;
    ic_complete_word(cenv, prefix, completion_add, NULL);
    s_completion_prefix = NULL;
}
