#include "cli_vcp.h"
#include <furi_hal_usb_cdc.h>
#include <furi_hal.h>
#include <furi.h>
#include <stdint.h>
#include <toolbox/pipe.h>
#include <toolbox/cli/shell/cli_shell.h>
#include <toolbox/api_lock.h>
#include "cli_main_shell.h"
#include "cli_main_commands.h"

#define TAG "CliVcp"

#define USB_CDC_PKT_LEN   CDC_DATA_SZ
#define VCP_BUF_SIZE      (USB_CDC_PKT_LEN * 3)
#define VCP_IF_NUM        0
#define VCP_MESSAGE_Q_LEN 8

#ifdef CLI_VCP_TRACE
#define VCP_TRACE(...) FURI_LOG_T(__VA_ARGS__)
#else
#define VCP_TRACE(...)
#endif

typedef struct {
    enum {
        CliVcpMessageTypeEnable,
        CliVcpMessageTypeDisable,
    } type;
    FuriApiLock api_lock;
    union {};
} CliVcpMessage;

typedef enum {
    CliVcpInternalEventConnected,
    CliVcpInternalEventDisconnected,
    CliVcpInternalEventTxDone,
    CliVcpInternalEventRx,
} CliVcpInternalEvent;

struct CliVcp {
    FuriEventLoop* event_loop;
    FuriMessageQueue* message_queue; // <! external messages
    FuriMessageQueue* internal_evt_queue;

    bool is_enabled, is_connected;
    FuriHalUsbInterface* previous_interface;

    PipeSide* own_pipe;
    PipeSide* shell_pipe;
    volatile bool is_currently_transmitting;
    size_t previous_tx_length;

    CliRegistry* main_registry;
    CliShell* shell;
};

// ============
// Data copying
// ============

/**
 * Called in the following cases:
 *   - previous transfer has finished;
 *   - new data became available to send.
 */
static void cli_vcp_maybe_send_data(CliVcp* cli_vcp) {
    if(cli_vcp->is_currently_transmitting) return;
    if(!cli_vcp->own_pipe) return;

    uint8_t buf[USB_CDC_PKT_LEN];
    size_t to_receive_from_pipe = MIN(sizeof(buf), pipe_bytes_available(cli_vcp->own_pipe));
    size_t length = pipe_receive(cli_vcp->own_pipe, buf, to_receive_from_pipe);
    if(length > 0 || cli_vcp->previous_tx_length == USB_CDC_PKT_LEN) {
        VCP_TRACE(TAG, "cdc_send length=%zu", length);
        cli_vcp->is_currently_transmitting = true;
        furi_hal_cdc_send(VCP_IF_NUM, buf, length);
    }
    cli_vcp->previous_tx_length = length;
}

/**
 * Called in the following cases:
 *   - new data arrived at the endpoint;
 *   - data was read out of the pipe.
 */
static void cli_vcp_maybe_receive_data(CliVcp* cli_vcp) {
    if(!cli_vcp->own_pipe) return;
    if(pipe_spaces_available(cli_vcp->own_pipe) < USB_CDC_PKT_LEN) return;

    uint8_t buf[USB_CDC_PKT_LEN];
    size_t length = furi_hal_cdc_receive(VCP_IF_NUM, buf, sizeof(buf));
    VCP_TRACE(TAG, "cdc_receive length=%zu", length);
    furi_check(pipe_send(cli_vcp->own_pipe, buf, length) == length);
}

// =============
// CDC callbacks
// =============

static void cli_vcp_signal_internal_event(CliVcp* cli_vcp, CliVcpInternalEvent event) {
    furi_check(furi_message_queue_put(cli_vcp->internal_evt_queue, &event, 0) == FuriStatusOk);
}

static void cli_vcp_cdc_tx_done(void* context) {
    CliVcp* cli_vcp = context;
    cli_vcp->is_currently_transmitting = false;
    cli_vcp_signal_internal_event(cli_vcp, CliVcpInternalEventTxDone);
}

static void cli_vcp_cdc_rx(void* context) {
    CliVcp* cli_vcp = context;
    cli_vcp_signal_internal_event(cli_vcp, CliVcpInternalEventRx);
}

static void cli_vcp_cdc_state_callback(void* context, CdcState state) {
    CliVcp* cli_vcp = context;
    if(state == CdcStateDisconnected) {
        cli_vcp_signal_internal_event(cli_vcp, CliVcpInternalEventDisconnected);
    }
    // `Connected` events are generated by DTR going active
}

static void cli_vcp_cdc_ctrl_line_callback(void* context, CdcCtrlLine ctrl_lines) {
    CliVcp* cli_vcp = context;
    if(ctrl_lines & CdcCtrlLineDTR) {
        cli_vcp_signal_internal_event(cli_vcp, CliVcpInternalEventConnected);
    } else {
        cli_vcp_signal_internal_event(cli_vcp, CliVcpInternalEventDisconnected);
    }
}

static CdcCallbacks cdc_callbacks = {
    .tx_ep_callback = cli_vcp_cdc_tx_done,
    .rx_ep_callback = cli_vcp_cdc_rx,
    .state_callback = cli_vcp_cdc_state_callback,
    .ctrl_line_callback = cli_vcp_cdc_ctrl_line_callback,
    .config_callback = NULL,
};

// ======================
// Pipe callback handlers
// ======================

static void cli_vcp_data_from_shell(PipeSide* pipe, void* context) {
    UNUSED(pipe);
    CliVcp* cli_vcp = context;
    cli_vcp_maybe_send_data(cli_vcp);
}

static void cli_vcp_shell_ready(PipeSide* pipe, void* context) {
    UNUSED(pipe);
    CliVcp* cli_vcp = context;
    cli_vcp_maybe_receive_data(cli_vcp);
}

/**
 * Processes messages arriving from other threads
 */
static void cli_vcp_message_received(FuriEventLoopObject* object, void* context) {
    CliVcp* cli_vcp = context;
    CliVcpMessage message;
    furi_check(furi_message_queue_get(object, &message, 0) == FuriStatusOk);

    switch(message.type) {
    case CliVcpMessageTypeEnable:
        if(cli_vcp->is_enabled) break;
        FURI_LOG_D(TAG, "Enabling");
        cli_vcp->is_enabled = true;

        // switch usb mode
        cli_vcp->previous_interface = furi_hal_usb_get_config();
        furi_hal_usb_set_config(&usb_cdc_single, NULL);
        furi_hal_cdc_set_callbacks(VCP_IF_NUM, &cdc_callbacks, cli_vcp);
        break;

    case CliVcpMessageTypeDisable:
        if(!cli_vcp->is_enabled) break;
        FURI_LOG_D(TAG, "Disabling");
        cli_vcp->is_enabled = false;

        // restore usb mode
        furi_hal_cdc_set_callbacks(VCP_IF_NUM, NULL, NULL);
        furi_hal_usb_set_config(cli_vcp->previous_interface, NULL);
        break;
    }

    api_lock_unlock(message.api_lock);
}

/**
 * Processes messages arriving from CDC event callbacks
 */
static void cli_vcp_internal_event_happened(FuriEventLoopObject* object, void* context) {
    CliVcp* cli_vcp = context;
    CliVcpInternalEvent event;
    furi_check(furi_message_queue_get(object, &event, 0) == FuriStatusOk);

    switch(event) {
    case CliVcpInternalEventRx: {
        VCP_TRACE(TAG, "Rx");
        cli_vcp_maybe_receive_data(cli_vcp);
        break;
    }

    case CliVcpInternalEventTxDone: {
        VCP_TRACE(TAG, "TxDone");
        cli_vcp_maybe_send_data(cli_vcp);
        break;
    }

    case CliVcpInternalEventDisconnected: {
        if(!cli_vcp->is_connected) return;
        FURI_LOG_D(TAG, "Disconnected");
        cli_vcp->is_connected = false;

        // disconnect our side of the pipe
        pipe_detach_from_event_loop(cli_vcp->own_pipe);
        pipe_free(cli_vcp->own_pipe);
        cli_vcp->own_pipe = NULL;

        // wait for shell to stop
        cli_shell_join(cli_vcp->shell);
        cli_shell_free(cli_vcp->shell);
        pipe_free(cli_vcp->shell_pipe);
        break;
    }

    case CliVcpInternalEventConnected: {
        if(cli_vcp->is_connected) return;
        FURI_LOG_D(TAG, "Connected");
        cli_vcp->is_connected = true;

        // start shell thread
        PipeSideBundle bundle = pipe_alloc(VCP_BUF_SIZE, 1);
        cli_vcp->own_pipe = bundle.alices_side;
        cli_vcp->shell_pipe = bundle.bobs_side;
        pipe_attach_to_event_loop(cli_vcp->own_pipe, cli_vcp->event_loop);
        pipe_set_callback_context(cli_vcp->own_pipe, cli_vcp);
        pipe_set_data_arrived_callback(
            cli_vcp->own_pipe, cli_vcp_data_from_shell, FuriEventLoopEventFlagEdge);
        pipe_set_space_freed_callback(
            cli_vcp->own_pipe, cli_vcp_shell_ready, FuriEventLoopEventFlagEdge);
        furi_delay_ms(33); // we are too fast, minicom isn't ready yet
        cli_vcp->shell = cli_shell_alloc(
            cli_main_motd, NULL, cli_vcp->shell_pipe, cli_vcp->main_registry, &cli_main_ext_config);
        cli_shell_start(cli_vcp->shell);
        break;
    }
    }
}

// ============
// Thread stuff
// ============

static CliVcp* cli_vcp_alloc(void) {
    CliVcp* cli_vcp = malloc(sizeof(CliVcp));

    cli_vcp->event_loop = furi_event_loop_alloc();

    cli_vcp->message_queue = furi_message_queue_alloc(VCP_MESSAGE_Q_LEN, sizeof(CliVcpMessage));
    furi_event_loop_subscribe_message_queue(
        cli_vcp->event_loop,
        cli_vcp->message_queue,
        FuriEventLoopEventIn,
        cli_vcp_message_received,
        cli_vcp);

    cli_vcp->internal_evt_queue =
        furi_message_queue_alloc(VCP_MESSAGE_Q_LEN, sizeof(CliVcpInternalEvent));
    furi_event_loop_subscribe_message_queue(
        cli_vcp->event_loop,
        cli_vcp->internal_evt_queue,
        FuriEventLoopEventIn,
        cli_vcp_internal_event_happened,
        cli_vcp);

    cli_vcp->main_registry = furi_record_open(RECORD_CLI);

    return cli_vcp;
}

int32_t cli_vcp_srv(void* p) {
    UNUSED(p);

    if(!furi_hal_is_normal_boot()) {
        FURI_LOG_W(TAG, "Skipping start in special boot mode");
        furi_thread_suspend(furi_thread_get_current_id());
        return 0;
    }

    CliVcp* cli_vcp = cli_vcp_alloc();
    furi_record_create(RECORD_CLI_VCP, cli_vcp);
    furi_event_loop_run(cli_vcp->event_loop);

    return 0;
}

// ==========
// Public API
// ==========

static void cli_vcp_synchronous_request(CliVcp* cli_vcp, CliVcpMessage* message) {
    message->api_lock = api_lock_alloc_locked();
    furi_message_queue_put(cli_vcp->message_queue, message, FuriWaitForever);
    api_lock_wait_unlock_and_free(message->api_lock);
}

void cli_vcp_enable(CliVcp* cli_vcp) {
    furi_check(cli_vcp);
    CliVcpMessage message = {
        .type = CliVcpMessageTypeEnable,
    };
    cli_vcp_synchronous_request(cli_vcp, &message);
}

void cli_vcp_disable(CliVcp* cli_vcp) {
    furi_check(cli_vcp);
    CliVcpMessage message = {
        .type = CliVcpMessageTypeDisable,
    };
    cli_vcp_synchronous_request(cli_vcp, &message);
}
