#include "ad_binary_transport.h"

#include <stddef.h>
#include <string.h>

#include "ad_can_telemetry.h"
#include "ad_project_config.h"
#include "ad_usb_cdc.h"
#include "main.h"
#include "simulink_interface.h"

#ifndef AD_HOST_PROTOCOL_DEFAULT
#define AD_HOST_PROTOCOL_DEFAULT AD_HOST_PROTOCOL_MODBUS
#endif

#ifndef AD_BINARY_TELEMETRY_TRANSPORT_DEFAULT
#define AD_BINARY_TELEMETRY_TRANSPORT_DEFAULT AD_BINARY_TRANSPORT_USB
#endif

#ifndef AD_BINARY_TELEMETRY_PERIOD_MS
#define AD_BINARY_TELEMETRY_PERIOD_MS 10UL
#endif

static AD_BinaryTransportState_t g_binary_transport_state;

static void update_status_flags(void)
{
    uint32_t flags = 0UL;

    if (g_binary_transport_state.initialized != 0U) {
        flags |= AD_BINARY_TRANSPORT_STATUS_INITIALIZED;
    }
    if (AD_USB_CDC_GetState()->initialized != 0U) {
        flags |= AD_BINARY_TRANSPORT_STATUS_USB_READY;
    }
    if (AD_CAN_Telemetry_GetState()->started != 0U) {
        flags |= AD_BINARY_TRANSPORT_STATUS_CAN_READY;
    }
    if (AD_USB_CDC_IsConfigured() != 0U) {
        flags |= AD_BINARY_TRANSPORT_STATUS_USB_CONFIGURED;
    }
    if (AD_USB_CDC_IsTxBusy() != 0U) {
        flags |= AD_BINARY_TRANSPORT_STATUS_USB_TX_BUSY;
    }
    if (AD_CAN_Telemetry_IsTxBusy() != 0U) {
        flags |= AD_BINARY_TRANSPORT_STATUS_CAN_TX_BUSY;
    }
    if ((g_binary_transport_state.status_flags & AD_BINARY_TRANSPORT_STATUS_LAST_DROPPED) != 0UL) {
        flags |= AD_BINARY_TRANSPORT_STATUS_LAST_DROPPED;
    }

    g_binary_transport_state.usb_configured = AD_USB_CDC_IsConfigured();
    g_binary_transport_state.usb_tx_busy = AD_USB_CDC_IsTxBusy();
    g_binary_transport_state.can_started = AD_CAN_Telemetry_IsStarted();
    g_binary_transport_state.can_tx_busy = AD_CAN_Telemetry_IsTxBusy();
    g_binary_transport_state.status_flags = flags;
}

uint8_t AD_BinaryTransport_IsProtocolValid(uint16_t protocol)
{
    return (uint8_t)(((protocol == (uint16_t)AD_HOST_PROTOCOL_MODBUS) ||
                      (protocol == (uint16_t)AD_HOST_PROTOCOL_BINARY_TELEMETRY)) ? 1U : 0U);
}

uint8_t AD_BinaryTransport_IsTransportValid(uint16_t transport)
{
    return (uint8_t)(((transport == (uint16_t)AD_BINARY_TRANSPORT_USB) ||
                      (transport == (uint16_t)AD_BINARY_TRANSPORT_CAN) ||
                      (transport == (uint16_t)AD_BINARY_TRANSPORT_USB_AND_CAN)) ? 1U : 0U);
}

uint8_t AD_BinaryTransport_SetProtocol(uint16_t protocol)
{
    if (AD_BinaryTransport_IsProtocolValid(protocol) == 0U) {
        return 0U;
    }

    g_binary_transport_state.host_protocol = (uint8_t)protocol;
    update_status_flags();
    return 1U;
}

uint8_t AD_BinaryTransport_SetTransport(uint16_t transport)
{
    if (AD_BinaryTransport_IsTransportValid(transport) == 0U) {
        return 0U;
    }

    g_binary_transport_state.binary_transport = (uint8_t)transport;
    update_status_flags();
    return 1U;
}

uint8_t AD_BinaryTransport_GetProtocol(void)
{
    return g_binary_transport_state.host_protocol;
}

uint8_t AD_BinaryTransport_GetTransport(void)
{
    return g_binary_transport_state.binary_transport;
}

void AD_BinaryTransport_Init(void)
{
    uint16_t default_protocol = (uint16_t)AD_HOST_PROTOCOL_DEFAULT;
    uint16_t default_transport = (uint16_t)AD_BINARY_TELEMETRY_TRANSPORT_DEFAULT;

    memset(&g_binary_transport_state, 0, sizeof(g_binary_transport_state));
    g_binary_transport_state.period_ms = AD_BINARY_TELEMETRY_PERIOD_MS;

    if (AD_BinaryTransport_IsProtocolValid(default_protocol) == 0U) {
        default_protocol = (uint16_t)AD_HOST_PROTOCOL_MODBUS;
    }
    if (AD_BinaryTransport_IsTransportValid(default_transport) == 0U) {
        default_transport = (uint16_t)AD_BINARY_TRANSPORT_USB;
    }

    g_binary_transport_state.host_protocol = (uint8_t)default_protocol;
    g_binary_transport_state.binary_transport = (uint8_t)default_transport;

    AD_USB_CDC_Init();
    AD_CAN_Telemetry_Init();

    g_binary_transport_state.initialized = 1U;
    update_status_flags();
}

static uint8_t transport_uses_usb(void)
{
    return (uint8_t)(((g_binary_transport_state.binary_transport == (uint8_t)AD_BINARY_TRANSPORT_USB) ||
                      (g_binary_transport_state.binary_transport == (uint8_t)AD_BINARY_TRANSPORT_USB_AND_CAN)) ? 1U : 0U);
}

static uint8_t transport_uses_can(void)
{
    return (uint8_t)(((g_binary_transport_state.binary_transport == (uint8_t)AD_BINARY_TRANSPORT_CAN) ||
                      (g_binary_transport_state.binary_transport == (uint8_t)AD_BINARY_TRANSPORT_USB_AND_CAN)) ? 1U : 0U);
}

void AD_BinaryTransport_Loop(void)
{
    const uint8_t *bytes;
    size_t size;
    uint32_t now;
    uint8_t queued = 0U;
    uint8_t dropped = 0U;

    if (g_binary_transport_state.initialized == 0U) {
        return;
    }

    AD_USB_CDC_Loop();
    AD_CAN_Telemetry_Loop();

    update_status_flags();
    g_binary_transport_state.status_flags &= ~AD_BINARY_TRANSPORT_STATUS_LAST_DROPPED;

    if (g_binary_transport_state.host_protocol != (uint8_t)AD_HOST_PROTOCOL_BINARY_TELEMETRY) {
        update_status_flags();
        return;
    }

    now = HAL_GetTick();
    if ((now - g_binary_transport_state.last_tx_tick_ms) < g_binary_transport_state.period_ms) {
        update_status_flags();
        return;
    }

    bytes = SimulinkInterface_GetTelemetryBytes();
    size = SimulinkInterface_GetTelemetrySize();
    if ((bytes == 0) || (size == 0U) || (size > SIMULINK_TELEMETRY_MAX_BYTES)) {
        g_binary_transport_state.dropped_packets++;
        g_binary_transport_state.status_flags |= AD_BINARY_TRANSPORT_STATUS_LAST_DROPPED;
        update_status_flags();
        return;
    }

    if (transport_uses_usb() != 0U) {
        if (AD_USB_CDC_WritePacket(bytes, (uint16_t)size) != 0U) {
            queued = 1U;
            g_binary_transport_state.usb_packets++;
        } else {
            dropped = 1U;
        }
    }

    if (transport_uses_can() != 0U) {
        if (AD_CAN_Telemetry_QueuePacket(bytes, (uint16_t)size) != 0U) {
            queued = 1U;
            g_binary_transport_state.can_packets++;
        } else {
            dropped = 1U;
        }
    }

    if (queued != 0U) {
        g_binary_transport_state.tx_packets++;
        g_binary_transport_state.last_size_bytes = (uint32_t)size;
        g_binary_transport_state.last_tx_tick_ms = now;
    }

    if (dropped != 0U) {
        g_binary_transport_state.dropped_packets++;
        g_binary_transport_state.status_flags |= AD_BINARY_TRANSPORT_STATUS_LAST_DROPPED;
    }

    update_status_flags();
}

const AD_BinaryTransportState_t* AD_BinaryTransport_GetState(void)
{
    update_status_flags();
    return &g_binary_transport_state;
}
