// C STD lib
#include <assert.h>
#include <string.h>

// SDK
#include "driver/gpio.h"
#include "esp_err.h"
#include "esp_eth.h"
#include "esp_eth_driver.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_sleep.h"
#include "esp_wifi.h"
#include "ethernet_init.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/timers.h"

#include "types.hpp"

// Project includes
#include "network.hpp"
#include "led.hpp"

// Forward declarations
#if CONFIG_CALENDINK_BLINK_IP
internal esp_err_t get_ip_info(esp_netif_ip_info_t *ip_info);
internal void blink_last_ip_octet();
#endif

// Internal states
internal esp_netif_ip_info_t s_current_ip_info = {};

// === Ethernet ===

internal constexpr char kLogEthernet[] = "ETH";
internal constexpr char kNetifDescEth[] = "eth0";

internal esp_eth_handle_t *s_eth_handles = nullptr;
internal uint8_t s_eth_count = 0;
internal esp_eth_netif_glue_handle_t s_eth_glue = nullptr;
internal esp_netif_t *s_eth_netif = nullptr;
internal SemaphoreHandle_t s_semph_get_ip_addrs = nullptr;
internal SemaphoreHandle_t s_semph_eth_link = nullptr;
internal volatile bool s_eth_link_up = false;
#ifndef NDEBUG
internal bool s_ethernet_connected = false;
#endif

void ethernet_event_handler(void *arg, esp_event_base_t event_base,
                            int32_t event_id, void *event_data)
{
  if (event_base == IP_EVENT && event_id == IP_EVENT_ETH_GOT_IP)
  {
    ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
    if (event->esp_netif != s_eth_netif)
    {
      return;
    }
    ESP_LOGI(kLogEthernet, "Got IPv4 event: Interface \"%s\" address: " IPSTR,
             esp_netif_get_desc(event->esp_netif), IP2STR(&event->ip_info.ip));

    s_current_ip_info = event->ip_info;
    xSemaphoreGive(s_semph_get_ip_addrs);
  }
  else if (event_base == ETH_EVENT && event_id == ETHERNET_EVENT_CONNECTED)
  {
    ESP_LOGI(kLogEthernet, "Ethernet Link Up");
    s_eth_link_up = true;
    if (s_semph_eth_link)
    {
      xSemaphoreGive(s_semph_eth_link);
    }
  }
  else if (event_base == ETH_EVENT && event_id == ETHERNET_EVENT_DISCONNECTED)
  {
    ESP_LOGI(kLogEthernet, "Ethernet Link Down");
    s_eth_link_up = false;
    if (s_semph_eth_link)
    {
      xSemaphoreGive(s_semph_eth_link);
    }
  }
}

void teardown_ethernet()
{
  if (s_semph_eth_link != nullptr)
  {
    vSemaphoreDelete(s_semph_eth_link);
    s_semph_eth_link = nullptr;
  }

  if (s_semph_get_ip_addrs != nullptr)
  {
    vSemaphoreDelete(s_semph_get_ip_addrs);
    s_semph_get_ip_addrs = nullptr;
  }

  if (s_eth_netif)
  {

    ESP_ERROR_CHECK(esp_event_handler_unregister(IP_EVENT, IP_EVENT_ETH_GOT_IP,
                                                 &ethernet_event_handler));
    ESP_ERROR_CHECK(esp_event_handler_unregister(ETH_EVENT, ESP_EVENT_ANY_ID,
                                                 &ethernet_event_handler));
    ESP_ERROR_CHECK(esp_eth_stop(s_eth_handles[0]));
    ESP_ERROR_CHECK(esp_eth_del_netif_glue(s_eth_glue));
    esp_netif_destroy(s_eth_netif);
    ethernet_deinit_all(s_eth_handles);
  }

  s_eth_glue = nullptr;
  s_eth_netif = nullptr;
  s_eth_handles = nullptr;
  s_eth_count = 0;

  esp_netif_deinit();
}

esp_err_t connect_ethernet(bool blockUntilIPAcquired)
{
#ifndef NDEBUG
  assert(!s_ethernet_connected &&
         "Ethernet connect called but already connected!");
  s_ethernet_connected = true;
#endif

  s_semph_get_ip_addrs = xSemaphoreCreateBinary();
  s_semph_eth_link = xSemaphoreCreateBinary();
  if (s_semph_get_ip_addrs == NULL || s_semph_eth_link == NULL)
  {
    return ESP_ERR_NO_MEM;
  }
  // Connection is split in two steps. First we open the connection and ask for
  // an ip. Second a semaphor will block until the ip is acquired. If we dont
  // block then the user have to verify the semaphore before continuing.
  ESP_ERROR_CHECK(esp_netif_init());
  ESP_ERROR_CHECK(ethernet_init_all(&s_eth_handles, &s_eth_count));

  esp_netif_inherent_config_t esp_netif_config =
      ESP_NETIF_INHERENT_DEFAULT_ETH();
  // Warning: the interface desc is used in tests to capture actual connection
  // details (IP, gw, mask)
  esp_netif_config.if_desc = kNetifDescEth;
  esp_netif_config.route_prio = 64;
  esp_netif_config_t netif_config = {.base = &esp_netif_config,
                                     .driver = nullptr,
                                     .stack = ESP_NETIF_NETSTACK_DEFAULT_ETH};
  s_eth_netif = esp_netif_new(&netif_config);

  s_eth_glue = esp_eth_new_netif_glue(s_eth_handles[0]);
  esp_netif_attach(s_eth_netif, s_eth_glue);

  // Register user defined event handlers
  ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP,
                                             &ethernet_event_handler, NULL));
  ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID,
                                             &ethernet_event_handler, NULL));

  ESP_ERROR_CHECK(esp_eth_start(s_eth_handles[0]));

  // Will teardown connection properly on shutdown
  ESP_ERROR_CHECK(esp_register_shutdown_handler(&teardown_ethernet));

  if (blockUntilIPAcquired)
  {
    ESP_LOGI(kLogEthernet, "Waiting for IP address.");
    xSemaphoreTake(s_semph_get_ip_addrs, portMAX_DELAY);
#if CONFIG_CALENDINK_BLINK_IP
    blink_last_ip_octet();
#endif
  }

  return ESP_OK;
}

void disconnect_ethernet()
{
#ifndef NDEBUG
  assert(s_ethernet_connected &&
         "Ethernet disconnect called but not connected!");
  s_ethernet_connected = false;
#endif

  teardown_ethernet();
  ESP_ERROR_CHECK(esp_unregister_shutdown_handler(&teardown_ethernet));
}

esp_err_t check_ethernet_connection(uint32_t timeoutSeconds)
{
  // Wait up to 5000ms for the physical link to negotiate
  if (!s_eth_link_up)
  {
    if (!xSemaphoreTake(s_semph_eth_link, pdMS_TO_TICKS(5000)))
    {
      ESP_LOGE(
          kLogEthernet,
          "No physical Ethernet link detected (Timeout). Skipping DHCP wait.");
      return ESP_ERR_INVALID_STATE; // Special error to skip retries
    }

    if (!s_eth_link_up)
    {
      ESP_LOGE(kLogEthernet, "No physical Ethernet link detected "
                             "(Disconnected). Skipping DHCP wait.");
      return ESP_ERR_INVALID_STATE;
    }
  }

  ESP_LOGI(kLogEthernet, "Waiting for IP address for %d seconds.",
           timeoutSeconds);
  if (xSemaphoreTake(s_semph_get_ip_addrs,
                     pdMS_TO_TICKS(timeoutSeconds * 1000)))
  {
#if CONFIG_CALENDINK_BLINK_IP
    blink_last_ip_octet();
#endif
    return ESP_OK;
  }
  else
  {
    return ESP_ERR_TIMEOUT;
  }
}

// === WiFi ===

internal constexpr char kLogWifi[] = "WIFI";
internal esp_netif_t *s_wifi_netif = nullptr;
internal SemaphoreHandle_t s_semph_get_wifi_ip_addrs = nullptr;
internal SemaphoreHandle_t s_semph_wifi_link = nullptr;
internal volatile bool s_wifi_link_up = false;

#ifndef NDEBUG
internal bool s_wifi_connected = false;
#endif

void wifi_event_handler(void *arg, esp_event_base_t event_base,
                        int32_t event_id, void *event_data)
{
  if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_CONNECTED)
  {
    ESP_LOGI(kLogWifi, "WiFi associated with AP.");
    s_wifi_link_up = true;
    if (s_semph_wifi_link)
    {
      xSemaphoreGive(s_semph_wifi_link);
    }
  }
  else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
  {
    ESP_LOGI(kLogWifi, "WiFi disconnected.");
    s_wifi_link_up = false;
    if (s_semph_wifi_link)
    {
      xSemaphoreGive(s_semph_wifi_link);
    }
  }
  else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
  {
    ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
    if (event->esp_netif != s_wifi_netif)
    {
      return;
    }
    ESP_LOGI(kLogWifi, "Got IPv4 event: Interface \"%s\" address: " IPSTR,
             esp_netif_get_desc(event->esp_netif), IP2STR(&event->ip_info.ip));

    s_current_ip_info = event->ip_info;
    xSemaphoreGive(s_semph_get_wifi_ip_addrs);
  }
}

void teardown_wifi()
{

  if (s_semph_wifi_link != nullptr)
  {
    vSemaphoreDelete(s_semph_wifi_link);
    s_semph_wifi_link = nullptr;
  }

  if (s_semph_get_wifi_ip_addrs != nullptr)
  {
    vSemaphoreDelete(s_semph_get_wifi_ip_addrs);
    s_semph_get_wifi_ip_addrs = nullptr;
  }

  esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_CONNECTED,
                               &wifi_event_handler);
  esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED,
                               &wifi_event_handler);
  esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP,
                               &wifi_event_handler);

  if (s_wifi_netif)
  {
    esp_wifi_disconnect();
    esp_wifi_stop();
    esp_wifi_deinit();
    esp_netif_destroy(s_wifi_netif);
    s_wifi_netif = nullptr;
  }
}

esp_err_t connect_wifi(const char *ssid, const char *password,
                                bool blockUntilIPAcquired)
{
#ifndef NDEBUG
  assert(!s_wifi_connected && "WiFi connect called but already connected!");
  s_wifi_connected = true;
#endif

  s_semph_get_wifi_ip_addrs = xSemaphoreCreateBinary();
  s_semph_wifi_link = xSemaphoreCreateBinary();
  if (s_semph_get_wifi_ip_addrs == NULL || s_semph_wifi_link == NULL)
  {
    return ESP_ERR_NO_MEM;
  }

  // esp_netif_init() is already called by Ethernet, but safe to call multiple
  // times or we can assume it's initialized.
  s_wifi_netif = esp_netif_create_default_wifi_sta();

  wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
  ESP_ERROR_CHECK(esp_wifi_init(&cfg));

  ESP_ERROR_CHECK(esp_event_handler_register(
      WIFI_EVENT, WIFI_EVENT_STA_CONNECTED, &wifi_event_handler, NULL));
  ESP_ERROR_CHECK(esp_event_handler_register(
      WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, &wifi_event_handler, NULL));
  ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
                                             &wifi_event_handler, NULL));

  wifi_config_t wifi_config = {};
  strncpy((char *)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid) - 1);
  strncpy((char *)wifi_config.sta.password, password,
          sizeof(wifi_config.sta.password) - 1);
  // Setting a default auth mode if password is provided
  wifi_config.sta.threshold.authmode =
      password[0] != '\0' ? WIFI_AUTH_WPA2_PSK : WIFI_AUTH_OPEN;

  ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
  ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
  ESP_ERROR_CHECK(esp_wifi_start());

  // Set Power Save mode based on sdkconfig selection
#if defined(CONFIG_CALENDINK_WIFI_PS_MAX_MODEM)
  ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MAX_MODEM));
#elif defined(CONFIG_CALENDINK_WIFI_PS_NONE)
  ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
#else
  ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MIN_MODEM));
#endif

  ESP_ERROR_CHECK(esp_wifi_connect());

  ESP_ERROR_CHECK(esp_register_shutdown_handler(&teardown_wifi));

  if (blockUntilIPAcquired)
  {
    ESP_LOGI(kLogWifi, "Waiting for IP address.");
    xSemaphoreTake(s_semph_get_wifi_ip_addrs, portMAX_DELAY);
#if CONFIG_CALENDINK_BLINK_IP
    blink_last_ip_octet();
#endif
  }

  return ESP_OK;
}

void disconnect_wifi()
{
#ifndef NDEBUG
  assert(s_wifi_connected && "WiFi disconnect called but not connected!");
  s_wifi_connected = false;
#endif

  teardown_wifi();
  ESP_ERROR_CHECK(esp_unregister_shutdown_handler(&teardown_wifi));
}

esp_err_t check_wifi_connection(uint32_t timeoutSeconds)
{
  // Wait up to 10000ms for the physical link to associate with the AP
  if (!s_wifi_link_up)
  {
    ESP_LOGI(kLogWifi,
             "Physical link is down. Requesting immediate AP association...");
    esp_err_t err = esp_wifi_connect();
    if (err != ESP_OK && err != ESP_ERR_WIFI_CONN)
    {
      ESP_LOGE(kLogWifi, "esp_wifi_connect failed: %s", esp_err_to_name(err));
    }

    if (!xSemaphoreTake(s_semph_wifi_link, pdMS_TO_TICKS(10000)))
    {
      ESP_LOGE(kLogWifi, "Failed to associate with WiFi AP (Timeout).");
      return ESP_ERR_TIMEOUT; // Return timeout so main.cpp triggers a retry
    }

    // After semaphore is taken, check if it was because of a disconnect
    if (!s_wifi_link_up)
    {
      ESP_LOGE(kLogWifi, "Failed to associate with WiFi AP (Disconnected).");
      return ESP_ERR_TIMEOUT;
    }
  }

  ESP_LOGI(kLogWifi, "Waiting for IP address for %d seconds.", timeoutSeconds);
  if (xSemaphoreTake(s_semph_get_wifi_ip_addrs,
                     pdMS_TO_TICKS(timeoutSeconds * 1000)))
  {
#if CONFIG_CALENDINK_BLINK_IP
    blink_last_ip_octet();
#endif
    return ESP_OK;
  }
  else
  {
    return ESP_ERR_TIMEOUT;
  }
}

#if CONFIG_CALENDINK_BLINK_IP
internal esp_err_t get_ip_info(esp_netif_ip_info_t *ip_info)
{
  *ip_info = s_current_ip_info;
  return ESP_OK;
}

internal void blink_last_ip_octet()
{
  esp_netif_ip_info_t ip_info;
  if (get_ip_info(&ip_info) == ESP_OK)
  {
    uint8_t last_octet = (ip_info.ip.addr >> 24) & 0xFF;
    printf("IP Address: " IPSTR "\n", IP2STR(&ip_info.ip));

    // Blink digits of last_octet
    int h = last_octet / 100;
    int t = (last_octet / 10) % 10;
    int u = last_octet % 10;

    if (h > 0)
    {
      led_blink_number(h, 255, 255, 255);
    }
    if (h > 0 || t > 0)
    {
      led_blink_number(t, 255, 255, 255);
    }
    led_blink_number(u, 255, 255, 255);
  }
}
#endif
