/*
 * SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <string.h>
#include <stdint.h>
#include "sdkconfig.h"
#include "net_connect.h"
#include "net_connect_private.h"

#if CONFIG_NET_CONNECT_PPP
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_netif_ppp.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#if CONFIG_NET_CONNECT_PPP_DEVICE_USB
#include "tinyusb.h"
#include "tinyusb_cdc_acm.h"
#include "tinyusb_default_config.h"

static int s_itf = TINYUSB_CDC_ACM_0;  // Initialize to default CDC port, updated by line_state_changed callback
static uint8_t buf[CONFIG_TINYUSB_CDC_RX_BUFSIZE];

#else // DEVICE is UART

#include "driver/uart.h"
#define BUF_SIZE (1024)
static bool s_stop_task = false;

#endif // CONNECT_PPP_DEVICE


static const char *TAG = "net_connect_ppp";
static int s_retry_num = 0;
static EventGroupHandle_t s_event_group = NULL;
static esp_netif_t *s_netif;
static const int GOT_IPV4 = BIT0;
static const int CONNECTION_FAILED = BIT1;
#if CONFIG_NET_CONNECT_IPV6
static const int GOT_IPV6 = BIT2;
#define CONNECT_BITS (GOT_IPV4|GOT_IPV6|CONNECTION_FAILED)
#else
#define CONNECT_BITS (GOT_IPV4|CONNECTION_FAILED)
#endif

static esp_err_t transmit(void *h, void *buffer, size_t len)
{
    ESP_LOG_BUFFER_HEXDUMP(TAG, buffer, len, ESP_LOG_VERBOSE);
#if CONFIG_NET_CONNECT_PPP_DEVICE_USB
    // s_itf is initialized to TINYUSB_CDC_ACM_0 by default, and may be updated by line_state_changed callback
    tinyusb_cdcacm_write_queue(s_itf, buffer, len);
    tinyusb_cdcacm_write_flush(s_itf, 0);
#else // DEVICE_UART
    uart_write_bytes(UART_NUM_1, buffer, len);
#endif // CONNECT_PPP_DEVICE
    return ESP_OK;
}

static esp_netif_driver_ifconfig_t driver_cfg = {
    .handle = (void *)1,    // singleton driver, just to != NULL
    .transmit = transmit,
};
const esp_netif_driver_ifconfig_t *ppp_driver_cfg = &driver_cfg;

static void on_ip_event(void *arg, esp_event_base_t event_base,
                        int32_t event_id, void *event_data)
{

    if (event_id == IP_EVENT_PPP_GOT_IP) {
        ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
        if (!net_connect_is_our_netif(NET_CONNECT_NETIF_DESC_PPP, event->esp_netif)) {
            return;
        }
        esp_netif_t *netif = event->esp_netif;
        esp_netif_dns_info_t dns_info;
        ESP_LOGI(TAG, "Got IPv4 event: Interface \"%s\" address: " IPSTR, esp_netif_get_desc(event->esp_netif), IP2STR(&event->ip_info.ip));
        esp_netif_get_dns_info(netif, ESP_NETIF_DNS_MAIN, &dns_info);
        ESP_LOGI(TAG, "Main DNS server : " IPSTR, IP2STR(&dns_info.ip.u_addr.ip4));
        if (s_event_group != NULL) {
            xEventGroupSetBits(s_event_group, GOT_IPV4);
        }
#if CONFIG_NET_CONNECT_IPV6
    } else if (event_id == IP_EVENT_GOT_IP6) {
        ip_event_got_ip6_t *event = (ip_event_got_ip6_t *)event_data;
        if (!net_connect_is_our_netif(NET_CONNECT_NETIF_DESC_PPP, event->esp_netif)) {
            return;
        }
        esp_ip6_addr_type_t ipv6_type = esp_netif_ip6_get_addr_type(&event->ip6_info.ip);
        ESP_LOGI(TAG, "Got IPv6 event: Interface \"%s\" address: " IPV6STR ", type: %s", esp_netif_get_desc(event->esp_netif),
                 IPV62STR(event->ip6_info.ip), net_connect_ipv6_addr_types_to_str[ipv6_type]);
        if (ipv6_type == NET_CONNECT_PREFERRED_IPV6_TYPE) {
            if (s_event_group != NULL) {
                xEventGroupSetBits(s_event_group, GOT_IPV6);
            }
        }
#endif
    } else if (event_id == IP_EVENT_PPP_LOST_IP) {
        ESP_LOGI(TAG, "Disconnect from PPP Server");
        s_retry_num++;
        if (s_retry_num > CONFIG_NET_CONNECT_PPP_CONN_MAX_RETRY) {
            ESP_LOGE(TAG, "PPP Connection failed %d times, stop reconnecting.", s_retry_num);
            if (s_event_group != NULL) {
                xEventGroupSetBits(s_event_group, CONNECTION_FAILED);
            }
        } else {
            ESP_LOGI(TAG, "PPP Connection failed %d times, try to reconnect.", s_retry_num);
            esp_netif_action_start(s_netif, 0, 0, 0);
            esp_netif_action_connected(s_netif, 0, 0, 0);
        }

    }
}

#if CONFIG_NET_CONNECT_PPP_DEVICE_USB
static void cdc_rx_callback(int itf, cdcacm_event_t *event)
{
    size_t rx_size = 0;
    // Only process data from the interface we're using
    if (itf != s_itf) {
        // Not our channel
        return;
    }
    // Check if netif is still valid (not destroyed during shutdown)
    if (s_netif == NULL) {
        return;
    }
    esp_err_t ret = tinyusb_cdcacm_read(itf, buf, CONFIG_TINYUSB_CDC_RX_BUFSIZE, &rx_size);
    if (ret == ESP_OK) {
        ESP_LOG_BUFFER_HEXDUMP(TAG, buf, rx_size, ESP_LOG_VERBOSE);
        // pass the received data to the network interface
        esp_netif_receive(s_netif, buf, rx_size, NULL);
    } else {
        ESP_LOGE(TAG, "Read error");
    }
}

static void line_state_changed(int itf, cdcacm_event_t *event)
{
    s_itf = itf; // use this channel for the netif communication
    ESP_LOGI(TAG, "Line state changed on channel %d", itf);
}
#else // DEVICE is UART

static void ppp_task(void *args)
{
    uart_config_t uart_config = {};
    uart_config.baud_rate = CONFIG_NET_CONNECT_UART_BAUDRATE;
    uart_config.data_bits = UART_DATA_8_BITS;
    uart_config.parity    = UART_PARITY_DISABLE;
    uart_config.stop_bits = UART_STOP_BITS_1;
    uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
    uart_config.source_clk = UART_SCLK_DEFAULT;

    QueueHandle_t event_queue;
    ESP_ERROR_CHECK(uart_driver_install(UART_NUM_1, BUF_SIZE, 0, 16, &event_queue, 0));
    ESP_ERROR_CHECK(uart_param_config(UART_NUM_1, &uart_config));
    ESP_ERROR_CHECK(uart_set_pin(UART_NUM_1, CONFIG_NET_CONNECT_UART_TX_PIN, CONFIG_NET_CONNECT_UART_RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
    ESP_ERROR_CHECK(uart_set_rx_timeout(UART_NUM_1, 1));

    char *buffer = (char*)malloc(BUF_SIZE);
    if (buffer == NULL) {
        ESP_LOGE(TAG, "Failed to allocate memory for UART buffer");
        uart_driver_delete(UART_NUM_1);
        if (s_event_group != NULL) {
            xEventGroupSetBits(s_event_group, CONNECTION_FAILED);
        }
        vTaskDelete(NULL);
        return;
    }
    uart_event_t event;
    esp_event_handler_register(IP_EVENT, IP_EVENT_PPP_GOT_IP, esp_netif_action_connected, s_netif);
    esp_netif_action_start(s_netif, 0, 0, 0);
    esp_netif_action_connected(s_netif, 0, 0, 0);
    while (!s_stop_task) {
        if (xQueueReceive(event_queue, &event, pdMS_TO_TICKS(1000)) == pdTRUE) {
            if (event.type == UART_DATA) {
                // Only process data when timeout occurs (end of frame)
                // This allows accumulating more data (up to BUF_SIZE) before transfer
                // and ensures proper PPP frame separation based on silence time
                if (event.timeout_flag) {
                    size_t len;
                    uart_get_buffered_data_len(UART_NUM_1, &len);
                    if (len) {
                        len = uart_read_bytes(UART_NUM_1, buffer, BUF_SIZE, 0);
                        ESP_LOG_BUFFER_HEXDUMP(TAG, buffer, len, ESP_LOG_VERBOSE);
                        esp_netif_receive(s_netif, buffer, len, NULL);
                    }
                }
                // Ignore FIFO-full events (timeout_flag = false) to accumulate more data
            } else {
                ESP_LOGW(TAG, "Received UART event: %d", event.type);
            }
        }
    }
    // Unregister event handler to prevent events after s_netif is destroyed
    esp_event_handler_unregister(IP_EVENT, IP_EVENT_PPP_GOT_IP, esp_netif_action_connected);
    // Clean up UART driver
    uart_driver_delete(UART_NUM_1);
    free(buffer);
    vTaskDelete(NULL);
}

#endif // CONNECT_PPP_DEVICE

static void cleanup_on_failure(void)
{
    // Unregister event handler
    esp_event_handler_unregister(IP_EVENT, ESP_EVENT_ANY_ID, on_ip_event);
#if CONFIG_NET_CONNECT_PPP_DEVICE_USB
    // Clean up TinyUSB resources
    tinyusb_cdcacm_register_callback(TINYUSB_CDC_ACM_0, CDC_EVENT_LINE_STATE_CHANGED, NULL);
    tinyusb_driver_uninstall();
#elif CONFIG_NET_CONNECT_PPP_DEVICE_UART
    // Stop the PPP task in UART mode
    s_stop_task = true;
    vTaskDelay(pdMS_TO_TICKS(1000)); // wait for the ppp task to stop
#endif
    // Disconnect and destroy netif
    if (s_netif != NULL) {
        esp_netif_action_disconnected(s_netif, 0, 0, 0);
        esp_netif_action_stop(s_netif, 0, 0, 0);
        esp_netif_destroy(s_netif);
        s_netif = NULL;
    }
    // Delete event group
    if (s_event_group != NULL) {
        vEventGroupDelete(s_event_group);
        s_event_group = NULL;
    }
}

esp_err_t net_connect_ppp_connect(void)
{
    s_retry_num = 0;
    ESP_LOGI(TAG, "Start example_connect.");

#if CONFIG_NET_CONNECT_PPP_DEVICE_USB
    ESP_LOGI(TAG, "USB initialization");
    tinyusb_config_t tusb_cfg = TINYUSB_DEFAULT_CONFIG();

    ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg));

    tinyusb_config_cdcacm_t acm_cfg = {
        .cdc_port = TINYUSB_CDC_ACM_0,
        .callback_rx = &cdc_rx_callback,
        .callback_rx_wanted_char = NULL,
        .callback_line_state_changed = NULL,
        .callback_line_coding_changed = NULL
    };

    ESP_ERROR_CHECK(tinyusb_cdcacm_init(&acm_cfg));
    /* the second way to register a callback */
    ESP_ERROR_CHECK(tinyusb_cdcacm_register_callback(
                        TINYUSB_CDC_ACM_0,
                        CDC_EVENT_LINE_STATE_CHANGED,
                        &line_state_changed));
#endif // CONFIG_NET_CONNECT_PPP_DEVICE_USB

    s_event_group = xEventGroupCreate();
    if (s_event_group == NULL) {
        ESP_LOGE(TAG, "Failed to create event group");
#if CONFIG_NET_CONNECT_PPP_DEVICE_USB
        // Clean up TinyUSB resources before returning
        tinyusb_cdcacm_register_callback(TINYUSB_CDC_ACM_0, CDC_EVENT_LINE_STATE_CHANGED, NULL);
        tinyusb_driver_uninstall();
#endif // CONFIG_NET_CONNECT_PPP_DEVICE_USB
        return ESP_ERR_NO_MEM;
    }

    ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, on_ip_event, NULL));

    esp_netif_inherent_config_t base_netif_cfg = ESP_NETIF_INHERENT_DEFAULT_PPP();
    base_netif_cfg.if_desc = NET_CONNECT_NETIF_DESC_PPP;
    esp_netif_config_t netif_ppp_config = { .base = &base_netif_cfg,
                                            .driver = ppp_driver_cfg,
                                            .stack = ESP_NETIF_NETSTACK_DEFAULT_PPP
                                          };

    s_netif = esp_netif_new(&netif_ppp_config);
    assert(s_netif);
#if CONFIG_NET_CONNECT_PPP_DEVICE_USB
    esp_netif_action_start(s_netif, 0, 0, 0);
    esp_netif_action_connected(s_netif, 0, 0, 0);
#else // DEVICE is UART
    s_stop_task = false;
    if (xTaskCreate(ppp_task, "ppp connect", 4096, NULL, 5, NULL) != pdTRUE) {
        ESP_LOGE(TAG, "Failed to create a ppp connection task");
        cleanup_on_failure();
        return ESP_FAIL;
    }
#endif // CONNECT_PPP_DEVICE

    ESP_LOGI(TAG, "Waiting for IP address");
#if CONFIG_NET_CONNECT_IPV6
    while (true) {
        EventBits_t bits = xEventGroupWaitBits(
                               s_event_group,
                               GOT_IPV4 | GOT_IPV6 | CONNECTION_FAILED,
                               pdFALSE,
                               pdFALSE,
                               portMAX_DELAY
                           );

        if (bits & CONNECTION_FAILED) {
            ESP_LOGE(TAG, "Connection failed!");
            cleanup_on_failure();
            return ESP_FAIL;
        }

        if ((bits & (GOT_IPV4 | GOT_IPV6)) == (GOT_IPV4 | GOT_IPV6)) {
            ESP_LOGI(TAG, "Connected!");
            return ESP_OK;
        }
    }
#else
    // When IPv6 is disabled, wait for IPv4 only
    EventBits_t bits = xEventGroupWaitBits(s_event_group, CONNECT_BITS, pdFALSE, pdFALSE, portMAX_DELAY);
    if (bits & CONNECTION_FAILED) {
        ESP_LOGE(TAG, "Connection failed!");
        cleanup_on_failure();
        return ESP_FAIL;
    }
    ESP_LOGI(TAG, "Connected!");

    return ESP_OK;
#endif
}

void net_connect_ppp_shutdown(void)
{
    ESP_ERROR_CHECK(esp_event_handler_unregister(IP_EVENT, ESP_EVENT_ANY_ID, on_ip_event));
#if CONFIG_NET_CONNECT_PPP_DEVICE_USB
    // Unregister CDC line state callback to prevent access to destroyed netif
    // Note: RX callback is set via config and cannot be unregistered, but cdc_rx_callback
    // has a NULL check to prevent crashes if called after shutdown
    tinyusb_cdcacm_register_callback(TINYUSB_CDC_ACM_0, CDC_EVENT_LINE_STATE_CHANGED, NULL);
#elif CONFIG_NET_CONNECT_PPP_DEVICE_UART
    s_stop_task = true;
    vTaskDelay(pdMS_TO_TICKS(1000)); // wait for the ppp task to stop
#endif

    // Stop netif first to allow PPP to send closing frames while TinyUSB is still active
    if (s_netif != NULL) {
        esp_netif_action_disconnected(s_netif, 0, 0, 0);
        esp_netif_action_stop(s_netif, 0, 0, 0);
        // Wait a bit for PPP close frames to be sent
        vTaskDelay(pdMS_TO_TICKS(100));
        esp_netif_destroy(s_netif);
        s_netif = NULL;
    }

#if CONFIG_NET_CONNECT_PPP_DEVICE_USB
    // Uninstall TinyUSB driver to free USB resources (after netif is stopped)
    tinyusb_driver_uninstall();
    // Reset interface to default after uninstall
    s_itf = TINYUSB_CDC_ACM_0;
#endif

    if (s_event_group != NULL) {
        vEventGroupDelete(s_event_group);
        s_event_group = NULL;
    }
}

#endif // CONFIG_NET_CONNECT_PPP
