#include "esp8266.h"
#include "usart.h"
#include "variables.h"
#include <string.h>
#include <stdio.h>

typedef struct
{
    uint16_t len;
    uint8_t data[128];
} EspTxPacket_t;

enum
{
    ESP8266_TASK_RX_FLAG = 0x01U
};

#define ESP8266_UART_RX_FIFO_SIZE   512U
#define ESP8266_APP_RX_FIFO_SIZE    512U
#define ESP8266_RESP_BUF_SIZE       256U
#define ESP8266_TX_QUEUE_DEPTH      8U
#define ESP8266_AT_TIMEOUT_MS       1000U
#define ESP8266_WAIT_WIFI_TIMEOUT_MS 30000U
#define ESP8266_LINK_TIMEOUT_MS     12000U

static const char esp8266_host[] = "124.221.53.178";
static const uint16_t esp8266_port = 41164U;

static osThreadId_t esp8266TaskHandle = NULL;
static osMutexId_t esp8266TxMutex = NULL;
static osMessageQueueId_t esp8266TxQueue = NULL;

static volatile uint16_t uart4_rx_head = 0U;
static volatile uint16_t uart4_rx_tail = 0U;
static uint8_t uart4_rx_fifo[ESP8266_UART_RX_FIFO_SIZE];

static volatile uint16_t app_rx_head = 0U;
static volatile uint16_t app_rx_tail = 0U;
static uint8_t app_rx_fifo[ESP8266_APP_RX_FIFO_SIZE];

static volatile uint8_t esp8266_transparent_ready = 0U;
static volatile uint8_t esp8266_link_up = 0U;
static volatile uint8_t esp8266_wifi_got_ip = 0U;
static volatile uint32_t esp8266_rx_drop_count = 0U;
static volatile uint32_t esp8266_tx_drop_count = 0U;

static const osMutexAttr_t esp8266_tx_mutex_attr = {
    .name = "esp8266TxMutex"
};

static const osMessageQueueAttr_t esp8266_tx_queue_attr = {
    .name = "esp8266TxQueue"
};

static void esp8266_reset_app_fifo(void)
{
    app_rx_head = 0U;
    app_rx_tail = 0U;
}

static void esp8266_reset_link_state(void)
{
    esp8266_transparent_ready = 0U;
    esp8266_link_up = 0U;
    esp_link_ready = 0U;
}

static void esp8266_flush_uart_fifo(void)
{
    while (uart4_rx_head != uart4_rx_tail)
    {
        uart4_rx_tail = (uint16_t)((uart4_rx_tail + 1U) % ESP8266_UART_RX_FIFO_SIZE);
    }
}

static uint8_t esp8266_uart_fifo_pop(uint8_t *byte)
{
    if (uart4_rx_head == uart4_rx_tail)
    {
        return 0U;
    }

    *byte = uart4_rx_fifo[uart4_rx_tail];
    uart4_rx_tail = (uint16_t)((uart4_rx_tail + 1U) % ESP8266_UART_RX_FIFO_SIZE);
    return 1U;
}

static void esp8266_app_fifo_push(uint8_t byte)
{
    const uint16_t next = (uint16_t)((app_rx_head + 1U) % ESP8266_APP_RX_FIFO_SIZE);

    if (next == app_rx_tail)
    {
        esp8266_rx_drop_count++;
        return;
    }

    app_rx_fifo[app_rx_head] = byte;
    app_rx_head = next;
}

static uint8_t esp8266_app_fifo_pop(uint8_t *byte)
{
    if (app_rx_head == app_rx_tail)
    {
        return 0U;
    }

    *byte = app_rx_fifo[app_rx_tail];
    app_rx_tail = (uint16_t)((app_rx_tail + 1U) % ESP8266_APP_RX_FIFO_SIZE);
    return 1U;
}

static HAL_StatusTypeDef esp8266_uart_send_bytes(const uint8_t *data, uint16_t len, uint32_t timeout_ms)
{
    if ((data == NULL) || (len == 0U))
    {
        return HAL_ERROR;
    }

    return HAL_UART_Transmit(&huart4, (uint8_t *)data, len, timeout_ms);
}

static HAL_StatusTypeDef esp8266_uart_send_string(const char *text)
{
    if (text == NULL)
    {
        return HAL_ERROR;
    }

    return esp8266_uart_send_bytes((const uint8_t *)text, (uint16_t)strlen(text), 200U);
}

static void esp8266_consume_uart_fifo_to_app(void)
{
    uint8_t byte = 0U;

    while (esp8266_uart_fifo_pop(&byte) != 0U)
    {
        esp8266_app_fifo_push(byte);
        esp_rx_byte_count++;
    }
}

static uint8_t esp8266_wait_response(const char *expect1,
                                     const char *expect2,
                                     const char *reject,
                                     uint32_t timeout_ms,
                                     char *out,
                                     size_t out_size)
{
    const uint32_t start = HAL_GetTick();
    size_t used = 0U;
    uint8_t byte = 0U;

    if ((out != NULL) && (out_size > 0U))
    {
        out[0] = '\0';
    }

    for (;;)
    {
        while (esp8266_uart_fifo_pop(&byte) != 0U)
        {
            esp8266_app_fifo_push(byte);
            esp_rx_byte_count++;

            if ((out != NULL) && (out_size > 1U))
            {
                if (used < (out_size - 1U))
                {
                    out[used++] = (char)byte;
                    out[used] = '\0';
                }
                else
                {
                    memmove(out, out + 1, out_size - 2U);
                    out[out_size - 2U] = (char)byte;
                    out[out_size - 1U] = '\0';
                }

                if ((reject != NULL) && (strstr(out, reject) != NULL))
                {
                    return 0U;
                }
                if ((expect1 != NULL) && (strstr(out, expect1) != NULL))
                {
                    return 1U;
                }
                if ((expect2 != NULL) && (strstr(out, expect2) != NULL))
                {
                    return 1U;
                }
            }
        }

        if ((HAL_GetTick() - start) >= timeout_ms)
        {
            return 0U;
        }

        (void)osThreadFlagsWait(ESP8266_TASK_RX_FLAG, osFlagsWaitAny, 10U);
    }
}

static uint8_t esp8266_send_cmd(const char *cmd,
                                const char *expect1,
                                const char *expect2,
                                uint32_t timeout_ms)
{
    char cmd_buf[160] = {0};
    char resp_buf[ESP8266_RESP_BUF_SIZE] = {0};

    if (cmd == NULL)
    {
        return 0U;
    }

    esp8266_flush_uart_fifo();
    (void)snprintf(cmd_buf, sizeof(cmd_buf), "%s\r\n", cmd);
    (void)esp8266_uart_send_string(cmd_buf);
    return esp8266_wait_response(expect1, expect2, "ERROR", timeout_ms, resp_buf, sizeof(resp_buf));
}

static uint8_t esp8266_send_cmd_expect_ok(const char *cmd, uint32_t timeout_ms)
{
    return esp8266_send_cmd(cmd, "OK", NULL, timeout_ms);
}

static uint8_t esp8266_enter_transparent_mode(void)
{
    char resp_buf[ESP8266_RESP_BUF_SIZE] = {0};

    esp8266_flush_uart_fifo();
    if (esp8266_uart_send_string("AT+CIPSEND\r\n") != HAL_OK)
    {
        return 0U;
    }

    return esp8266_wait_response(">", NULL, "ERROR", ESP8266_AT_TIMEOUT_MS, resp_buf, sizeof(resp_buf));
}

static uint8_t esp8266_wait_wifi_got_ip(uint32_t timeout_ms)
{
    const uint32_t start = HAL_GetTick();

    if (esp8266_wifi_got_ip != 0U)
    {
        return 1U;
    }

    for (;;)
    {
        esp8266_consume_uart_fifo_to_app();

        if (esp8266_wifi_got_ip != 0U)
        {
            return 1U;
        }

        if ((HAL_GetTick() - start) >= timeout_ms)
        {
            return 0U;
        }

        (void)osThreadFlagsWait(ESP8266_TASK_RX_FLAG, osFlagsWaitAny, 20U);
    }
}

static void esp8266_detect_link_status_byte(uint8_t byte)
{
    static char status_buf[32] = {0};
    static uint8_t idx = 0U;

    if ((byte == '\r') || (byte == '\n'))
    {
        if (idx > 0U)
        {
            status_buf[idx] = '\0';

            if (strcmp(status_buf, "WIFI GOT IP") == 0)
            {
                esp8266_wifi_got_ip = 1U;
                esp_link_stage = 3U;
            }
            else if (strcmp(status_buf, "WIFI CONNECTED") == 0)
            {
                esp_link_stage = 2U;
            }
            else if ((strcmp(status_buf, "WIFI DISCONNECT") == 0) ||
                     (strcmp(status_buf, "FAIL") == 0) ||
                     (strcmp(status_buf, "ERROR") == 0))
            {
                esp8266_wifi_got_ip = 0U;
                esp8266_reset_link_state();
            }
            else if ((strcmp(status_buf, "CONNECT") == 0) ||
                     (strcmp(status_buf, "ALREADY CONNECTED") == 0))
            {
                esp8266_link_up = 1U;
                esp_link_stage = 5U;
            }
            else if (strcmp(status_buf, "CLOSED") == 0)
            {
                esp8266_reset_link_state();
            }
        }
        idx = 0U;
        return;
    }

    if ((byte < 0x20U) || (byte > 0x7EU))
    {
        idx = 0U;
        return;
    }

    if (idx < (uint8_t)(sizeof(status_buf) - 1U))
    {
        status_buf[idx++] = (char)byte;
    }
    else
    {
        memmove(status_buf, status_buf + 1, sizeof(status_buf) - 2U);
        status_buf[sizeof(status_buf) - 2U] = (char)byte;
        status_buf[sizeof(status_buf) - 1U] = '\0';
    }
}

void ESP8266_RegisterTaskHandle(osThreadId_t tid)
{
    esp8266TaskHandle = tid;

    if (esp8266TxMutex == NULL)
    {
        esp8266TxMutex = osMutexNew(&esp8266_tx_mutex_attr);
    }

    if (esp8266TxQueue == NULL)
    {
        esp8266TxQueue = osMessageQueueNew(ESP8266_TX_QUEUE_DEPTH,
                                           sizeof(EspTxPacket_t),
                                           &esp8266_tx_queue_attr);
    }
}

void ESP8266_RxByteIRQ(uint8_t byte)
{
    const uint16_t next = (uint16_t)((uart4_rx_head + 1U) % ESP8266_UART_RX_FIFO_SIZE);

    esp8266_detect_link_status_byte(byte);

    if (next != uart4_rx_tail)
    {
        uart4_rx_fifo[uart4_rx_head] = byte;
        uart4_rx_head = next;
    }
    else
    {
        esp8266_rx_drop_count++;
    }

    if (esp8266TaskHandle != NULL)
    {
        (void)osThreadFlagsSet(esp8266TaskHandle, ESP8266_TASK_RX_FLAG);
    }
}

void ESP8266_RxErrorIRQ(void)
{
    if (esp8266TaskHandle != NULL)
    {
        (void)osThreadFlagsSet(esp8266TaskHandle, ESP8266_TASK_RX_FLAG);
    }
}

uint8_t ESP8266_Bringup(void)
{
    char cmd[128] = {0};

    esp8266_reset_link_state();
    esp8266_reset_app_fifo();

    if (esp8266_wait_wifi_got_ip(ESP8266_WAIT_WIFI_TIMEOUT_MS) == 0U)
    {
        return 0U;
    }

    if (esp8266_send_cmd_expect_ok("AT+CIPMUX=0", ESP8266_AT_TIMEOUT_MS) == 0U)
    {
        return 0U;
    }

    if (esp8266_send_cmd_expect_ok("AT+CIPMODE=1", ESP8266_AT_TIMEOUT_MS) == 0U)
    {
        return 0U;
    }
    esp_link_stage = 4U;

    (void)snprintf(cmd,
                   sizeof(cmd),
                   "AT+CIPSTART=\"TCP\",\"%s\",%u",
                   esp8266_host,
                   (unsigned int)esp8266_port);
    if (esp8266_send_cmd(cmd, "CONNECT", "ALREADY CONNECTED", ESP8266_LINK_TIMEOUT_MS) == 0U)
    {
        return 0U;
    }
    esp_link_stage = 5U;

    if (esp8266_enter_transparent_mode() == 0U)
    {
        return 0U;
    }

    esp8266_link_up = 1U;
    esp8266_transparent_ready = 1U;
    esp_link_ready = 1U;
    esp_link_stage = 6U;
    esp_link_ok_count++;
    return 1U;
}

void ESP8266_Service(void)
{
    uint8_t byte = 0U;
    EspTxPacket_t pkt;

    while (esp8266_uart_fifo_pop(&byte) != 0U)
    {
        esp8266_app_fifo_push(byte);
        esp_rx_byte_count++;
    }

    if ((esp8266_transparent_ready != 0U) && (esp8266TxQueue != NULL) && (esp8266TxMutex != NULL))
    {
        while (osMessageQueueGet(esp8266TxQueue, &pkt, NULL, 0U) == osOK)
        {
            if (osMutexAcquire(esp8266TxMutex, 100U) == osOK)
            {
                if (esp8266_uart_send_bytes(pkt.data, pkt.len, 200U) == HAL_OK)
                {
                    esp_tx_byte_count += pkt.len;
                }
                else
                {
                    esp8266_reset_link_state();
                    (void)osMutexRelease(esp8266TxMutex);
                    break;
                }
                (void)osMutexRelease(esp8266TxMutex);
            }
            else
            {
                break;
            }
        }
    }
}

uint8_t ESP8266_IsTransparentReady(void)
{
    return esp8266_transparent_ready;
}

uint8_t ESP8266_SendRaw(const uint8_t *data, uint16_t len, uint32_t timeout_ms)
{
    EspTxPacket_t pkt;

    if ((data == NULL) || (len == 0U) || (len > sizeof(pkt.data)))
    {
        return 0U;
    }

    if ((esp8266_transparent_ready == 0U) || (esp8266TxQueue == NULL))
    {
        return 0U;
    }

    pkt.len = len;
    memcpy(pkt.data, data, len);

    if (osMessageQueuePut(esp8266TxQueue, &pkt, 0U, timeout_ms) != osOK)
    {
        esp8266_tx_drop_count++;
        return 0U;
    }

    return 1U;
}

uint16_t ESP8266_ReadRaw(uint8_t *data, uint16_t max_len)
{
    uint16_t count = 0U;
    uint8_t byte = 0U;

    if ((data == NULL) || (max_len == 0U))
    {
        return 0U;
    }

    while ((count < max_len) && (esp8266_app_fifo_pop(&byte) != 0U))
    {
        data[count++] = byte;
    }

    return count;
}
