/*
 * Copyright (C) MOXA Inc. All rights reserved.
 * Authors:
 *     2024  Wilson YS Huang  <wilsonys.huang@moxa.com>
 * This software is distributed under the terms of the MOXA SOFTWARE NOTICE.
 * See the file LICENSE for details.
 */

#define _GNU_SOURCE
#include <errno.h>
#include <logger.h>
#include <mcu.h>
#include <tty.h>

#define APP_WDT_TICK_INTERVAL 1000
typedef struct {
    mcu_packet   *req_pkt;
    size_t        req_pkt_sz;
    mcu_packet   *resp_pkt;
    size_t        resp_pkt_sz;
    resp_callback resp_cb;
    void         *context;
} mcu_queue_node;

#define QUEUE_SIZE 10
typedef struct {
    mcu_queue_node *nodes[QUEUE_SIZE];
    uint32_t        front;
    uint32_t        rear;
    uint32_t        count;
    pthread_mutex_t lock;
    pthread_cond_t  not_empty;
    pthread_cond_t  not_full;
} mcu_queue;

static mcu_queue queue;
static pthread_t app_wdt_tid;
static bool      app_wdt_ticking;

static void print_mcu_packet(mcu_packet *pkt)
{
    log_debug("%-10s: 0x%02X", "attn", pkt->attn);
    log_debug("%-10s: 0x%02X", "addr", pkt->addr);
    log_debug("%-10s: 0x%02X 0x%02X 0x%02X", "cmd", pkt->command[0], pkt->command[1], pkt->command[2]);
    log_debug("%-10s: 0x%02X", "data size", pkt->data_sz);
    log_debug("%-10s: 0x%02X", "hdr chk", pkt->hdr_chk);

    if (pkt->data_sz > MAX_DATA_SZ) {
        log_debug("oversized data");
    }
    else {
        for (int32_t i = 0; i < pkt->data_sz; i++) {
            log_debug("data[%d]   : 0x%02X", i, pkt->data[i]);
        }
        log_debug("%-10s: 0x%02X", "data chk", pkt->data[pkt->data_sz]);
    }
}

static void *app_watchdog_tick_thread(void *arg)
{
    bool *ticking = (bool *)arg;

    log_info("Start app watchdog tick, interval: %d seconds", APP_WDT_TICK_INTERVAL);

    while (*ticking) {
        log_debug("App watchdog ticking...");
        mcu_hal_request((uint8_t *)MX_MCU_APP_WDT_TIMEOUT_CMD, NULL, 0, NULL, NULL);
        sleep_ms(APP_WDT_TICK_INTERVAL);
    }

    log_info("Stop app watchdog tick");
    return NULL;
}

static bool app_watchdog_callback(void *arg)
{
    uint8_t *data = (uint8_t *)arg;

    log_debug("app_wdt_tid: %ld", app_wdt_tid);

    if (data[0] == 0) {
        app_wdt_ticking = false;
        pthread_join(app_wdt_tid, NULL);
        app_wdt_tid = 0;
    }
    else if (data[0] == 1) {
        app_wdt_ticking = true;
        if (app_wdt_tid == 0) {
            if (pthread_create(&app_wdt_tid, NULL, app_watchdog_tick_thread, &app_wdt_ticking) != 0) {
                log_warn("Failed to create app watchdog tick thread");
                return false;
            }
            else {
                log_debug("Create app watchdog tick thread");
                pthread_setname_np(app_wdt_tid, "app watchdog tick");
            }
        }
    }
    else {
        return false;
    }

    return true;
}

static bool pre_firmware_upgrade_callback(void *arg)
{
    uint8_t *data = (uint8_t *)arg;

    if (data[0] == 1) {
        log_debug("upgrade start mode");
        if (app_wdt_ticking && app_wdt_tid != 0) {
            uint8_t app_wdt_timout[1];
            app_wdt_timout[0] = 0;
            app_watchdog_callback(app_wdt_timout);
        }
    }

    return true;
}

struct {
    char    *command;
    uint32_t feature_bit;
    bool (*pre_callback_fn)(void *arg);
    bool (*post_callback_fn)(void *arg);
} mcu_commands_tbl[] = {
    {MX_MCU_VERSION_CMD, FEAT_MCU_VERSION, NULL, NULL},
    {MX_MCU_RELAY_MODE_CMD, FEAT_MCU_RELAY_MODE, NULL, NULL},
    {MX_MCU_WDT_RESET_MODE_CMD, FEAT_MCU_WDT_RESET_MODE, NULL, NULL},
    {MX_MCU_WDT_RELAY_MODE_CMD, FEAT_MCU_WDT_RELAY_MODE, NULL, NULL},
    {MX_MCU_POWEROFF_RELAY_MODE_CMD, FEAT_MCU_POWEROFF_RELAY_MODE, NULL, NULL},
    {MX_MCU_APP_WDT_RELAY_MODE_CMD, FEAT_MCU_APP_WDT_RELAY_MODE, NULL, NULL},
    {MX_MCU_APP_WDT_RESET_MODE_CMD, FEAT_MCU_APP_WDT_RESET_MODE_CMD, NULL, NULL},
    {MX_MCU_APP_WDT_TIMEOUT_CMD, FEAT_MCU_APP_WDT_TIMEOUT_CMD, NULL, app_watchdog_callback},
    {MX_MCU_FW_UPGRADE_CMD, FEAT_MCU_FW_UPGRADE_CMD, pre_firmware_upgrade_callback, NULL},
    {MX_MCU_FW_MODEL_NAME_CMD, FEAT_MCU_MODEL_NAME_CMD, NULL, NULL},
    {MX_MCU_BRIGHTNESS_CMD, FEAT_MCU_BRIGHTNESS_CMD, NULL, NULL},
    {MX_MCU_SYSTEM_STATUE_CMD, FEAT_MCU_SYSTEM_STATUE_CMD, NULL, NULL},
    {MX_MCU_PANEL_TOUCH_CTRL_CMD, FEAT_MCU_PANEL_TOUCH_CTRL_CMD, NULL, NULL},
    {MX_MCU_SHOW_OSD_CMD, FEAT_MCU_SHOW_OSD_CMD, NULL, NULL},
    {NULL, -1, NULL, NULL}};

static uint8_t calculate_checksum(const uint8_t *data, size_t length)
{
    uint8_t checksum = 0;

    for (uint8_t i = 0; i < length; ++i) {
        checksum += data[i];
    }

    return checksum;
}

static void mcu_queue_init(mcu_queue *q)
{
    q->front = 0;
    q->rear  = 0;
    q->count = 0;
    pthread_mutex_init(&q->lock, NULL);
    pthread_cond_init(&q->not_empty, NULL);
    pthread_cond_init(&q->not_full, NULL);
}

static void mcu_enqueue(mcu_queue_node *node)
{
    mcu_queue_node *push_node = NULL;
    pthread_mutex_lock(&queue.lock);
    while (queue.count == QUEUE_SIZE) {
        pthread_cond_wait(&queue.not_full, &queue.lock);
    }

    push_node = (mcu_queue_node *)calloc(1, sizeof(mcu_queue_node));
    memcpy(push_node, node, sizeof(mcu_queue_node));

    queue.nodes[queue.rear] = push_node;
    queue.rear              = (queue.rear + 1) % QUEUE_SIZE;
    queue.count++;

    log_debug("node enqueue, front: %d, rear: %d, count: %d", queue.front, queue.rear, queue.count);
    pthread_cond_signal(&queue.not_empty);
    pthread_mutex_unlock(&queue.lock);
}

static mcu_queue_node *mcu_dequeue(void)
{
    mcu_queue_node *pop_node = NULL;
    pthread_mutex_lock(&queue.lock);
    while (queue.count == 0) {
        pthread_cond_wait(&queue.not_empty, &queue.lock);
    }

    pop_node    = queue.nodes[queue.front];
    queue.front = (queue.front + 1) % QUEUE_SIZE;
    queue.count--;
    queue.nodes[queue.front] = NULL;

    log_debug("node dequeue, front: %d, rear: %d, count: %d", queue.front, queue.rear, queue.count);
    pthread_cond_signal(&queue.not_full);
    pthread_mutex_unlock(&queue.lock);
    return pop_node;
}

static void queue_node_delete(mcu_queue_node *node)
{
    if (node == NULL) {
        return;
    }

    if (node->req_pkt != NULL) {
        free(node->req_pkt);
    }

    if (node->resp_pkt != NULL) {
        free(node->resp_pkt);
    }

    free(node);
}

static bool invoke_pre_callback_fn(uint8_t *command, void *data)
{
    if (command == NULL) {
        return false;
    }

    for (uint8_t i = 0; mcu_commands_tbl[i].command != NULL; ++i) {
        log_debug("Check MCU command %s", mcu_commands_tbl[i].command);
        if (!memcmp(command, mcu_commands_tbl[i].command, MX_MCU_CMD_SIZE) &&
            mcu_commands_tbl[i].pre_callback_fn != NULL) {
            return mcu_commands_tbl[i].pre_callback_fn(data);
        }
    }

    return false;
}

static bool invoke_post_callback_fn(uint8_t *command, void *data)
{
    if (command == NULL) {
        return false;
    }

    for (uint8_t i = 0; mcu_commands_tbl[i].command != NULL; ++i) {
        log_debug("Check MCU command %s", mcu_commands_tbl[i].command);
        if (!memcmp(command, mcu_commands_tbl[i].command, MX_MCU_CMD_SIZE) &&
            mcu_commands_tbl[i].post_callback_fn != NULL) {
            return mcu_commands_tbl[i].post_callback_fn(data);
        }
    }

    return false;
}

static bool validate_mcu_packet(mcu_packet *pkt)
{
    uint8_t hdr_chk;
    uint8_t data_chk;

    hdr_chk = 0xff - ((pkt->attn + pkt->addr + pkt->command[0] + pkt->command[1] + pkt->command[2] + pkt->data_sz) & 0xff);
    if (hdr_chk != pkt->hdr_chk) {
        return false;
    }

    if (pkt->data_sz > MAX_DATA_SZ) {
        return false;
    }

    if (pkt->data_sz != 0) {
        data_chk = 0xff - (calculate_checksum(pkt->data, pkt->data_sz) & 0xff);
        if (data_chk != pkt->data[pkt->data_sz]) {
            return false;
        }
    }

    return true;
}

// consumer
void *proxy_mcu_packet_service(void *arg)
{
    int32_t         mcu_fd = *(int32_t *)arg;
    mcu_queue_node *node   = NULL;
    ssize_t         rc     = -1;
    mcu_error_code  err_code;

    log_info("Start proxy MCU packet service");

    // TODO: Terminate condition
    while (1) {
        node = mcu_dequeue();

        log_debug("request packet size: %zu", node->req_pkt_sz);
        print_mcu_packet(node->req_pkt);

        invoke_pre_callback_fn(node->req_pkt->command, node->req_pkt->data);

        rc = write(mcu_fd, node->req_pkt, node->req_pkt_sz);
        tcdrain(mcu_fd);   // Waits until all output written to the object referred to by fd has been transmitted.

        sleep_ms(DELAY_SEND_PKT);

        node->resp_pkt = (mcu_packet *)calloc(1, sizeof(mcu_packet) + MAX_DATA_SZ + sizeof(uint8_t));

        errno = 0;
        rc    = read(mcu_fd, node->resp_pkt, sizeof(mcu_packet) + MAX_DATA_SZ + sizeof(uint8_t));
        if (rc == 0) {
            log_warn("MCU device disconnected gracefully.");
            queue_node_delete(node);
            break;
        }
        else if (rc < 0 && (errno == ECONNRESET || errno == EPIPE)) {
            log_error("MCU device disconnected forcefully.");
            queue_node_delete(node);
            break;
        }

        node->resp_pkt_sz = sizeof(mcu_packet) + node->resp_pkt->data_sz + sizeof(uint8_t);

        log_debug("response packet size: %zu", node->resp_pkt_sz);
        print_mcu_packet(node->resp_pkt);

        if (!validate_mcu_packet(node->resp_pkt)) {
            err_code = ERR_CODE_COMMAND_ILLEGAL_MCU_PKT;
        }
        else if (node->resp_pkt->attn == MX_MCU_ACK) {
            err_code = ERR_CODE_OK;
            invoke_post_callback_fn(node->resp_pkt->command, node->resp_pkt->data);
        }
        else if (node->resp_pkt->attn == MX_MCU_NACK) {
            err_code = ERR_CODE_COMMAND_NACK_MCU_PKT;
        }
        else {
            err_code = ERR_CODE_UNKNOWN;
        }

        if (node->resp_cb != NULL) {
            node->resp_cb(err_code, node->resp_pkt->data, node->resp_pkt->data_sz, node->context);
        }

        queue_node_delete(node);
    }

    log_info("Stop proxy MCU packet service");

    return NULL;
}

bool mcu_hal_init(const char *tty_name)
{
    static int32_t fd = -1;
    pthread_t      tid;

    fd = open_tty(tty_name);
    if (flock(fd, LOCK_EX | LOCK_NB) < 0) {
        log_error("Error: MCU device is locked.\n");
        return false;
    }

    mcu_queue_init(&queue);
    pthread_create(&tid, NULL, proxy_mcu_packet_service, &fd);
    return true;
}

static size_t make_mcu_packet(mcu_packet **mcu_pkt, uint8_t command[3], uint8_t *data, size_t data_sz)
{
    mcu_packet *pkt    = NULL;
    size_t      pkt_sz = 0;

    if (data_sz != 0) {
        pkt = (mcu_packet *)calloc(1, sizeof(mcu_packet) + data_sz + sizeof(uint8_t));
        memcpy(pkt->data, data, data_sz);
        pkt->data[data_sz] = 0xff - (calculate_checksum(data, data_sz) & 0xff);   // data checksum
        pkt_sz             = sizeof(mcu_packet) + data_sz + sizeof(uint8_t);
    }
    else {
        pkt          = (mcu_packet *)calloc(1, sizeof(mcu_packet) + 2 * sizeof(uint8_t));
        pkt->data[0] = 0x00;
        pkt->data[1] = 0xFF;
        pkt_sz       = sizeof(mcu_packet) + 2 * sizeof(uint8_t);
    }

    pkt->attn = MX_MCU_CMD;
    pkt->addr = MX_MCU_ADDR_BROADCAST;
    memcpy(pkt->command, command, MX_MCU_CMD_SIZE);
    pkt->data_sz = data_sz;
    pkt->hdr_chk = 0xff - ((pkt->attn + pkt->addr + pkt->command[0] + pkt->command[1] + pkt->command[2] + data_sz) & 0xff);

    *mcu_pkt = pkt;

    return pkt_sz;
}

// producer
void mcu_hal_request(uint8_t *command, uint8_t *data, size_t data_sz, resp_callback callback, void *callback_arg)
{
    mcu_queue_node node   = {0};
    mcu_packet    *pkt    = NULL;
    ssize_t        pkt_sz = 0;

    if (data_sz > MAX_DATA_SZ) {
        log_warn("Invalid data size %zu", data_sz);
        return;
    }

    pkt_sz = make_mcu_packet(&pkt, command, data, data_sz);

    node.req_pkt     = pkt;
    node.req_pkt_sz  = pkt_sz;
    node.resp_pkt    = NULL;
    node.resp_pkt_sz = 0;
    node.resp_cb     = callback;
    node.context     = callback_arg;

    mcu_enqueue(&node);
}

bool is_mcu_command_support(uint8_t *command, uint32_t features)
{
    if (command == NULL) {
        return false;
    }

    for (uint8_t i = 0; mcu_commands_tbl[i].command != NULL; ++i) {
        log_debug("Check MCU command %s", mcu_commands_tbl[i].command);
        if (!memcmp(command, mcu_commands_tbl[i].command, MX_MCU_CMD_SIZE) &&
            features & mcu_commands_tbl[i].feature_bit) {
            return true;
        }
    }

    return false;
}
