#ifdef ETHERNET_ENABLED

#include "SerialEthernetInterface.h"
#include "EthernetMac.h"
#include <SPI.h>
#include <EthernetUdp.h>

#define PIN_SPI1_MISO (29) // (0 + 29)
#define PIN_SPI1_MOSI (30) // (0 + 30)
#define PIN_SPI1_SCK (3)   // (0 + 3)

SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI);

#define PIN_ETHERNET_POWER_EN WB_IO2    // output, high to enable
#define PIN_ETHERNET_RESET 21
#define PIN_ETHERNET_SS 26

#define RECV_STATE_IDLE        0
#define RECV_STATE_HDR_FOUND   1
#define RECV_STATE_LEN1_FOUND  2
#define RECV_STATE_LEN2_FOUND  3

bool SerialEthernetInterface::begin() {

  ETHERNET_DEBUG_PRINTLN("Ethernet initializing");

  // WB_IO2 (power enable) is already driven HIGH by early constructor
  // in RAK4631Board.cpp to support POE boot.
  // Skip hardware reset — the W5100S comes out of power-on reset cleanly,
  // and toggling reset kills the PHY link which breaks POE power.
#ifdef PIN_ETHERNET_RESET
        pinMode(PIN_ETHERNET_RESET, OUTPUT);
        digitalWrite(PIN_ETHERNET_RESET, HIGH);
#endif

  uint8_t mac[6];
  generateEthernetMac(mac);
  ETHERNET_DEBUG_PRINTLN(
      "Ethernet MAC: %02X:%02X:%02X:%02X:%02X:%02X",
      mac[0],
      mac[1],
      mac[2],
      mac[3],
      mac[4],
      mac[5]);
  ETHERNET_DEBUG_PRINTLN("Init");
  ETHERNET_SPI_PORT.begin();
  Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS);

  // Use static IP if build flags are defined, otherwise DHCP
  #if defined(ETHERNET_STATIC_IP) && defined(ETHERNET_STATIC_GATEWAY) && defined(ETHERNET_STATIC_SUBNET) && defined(ETHERNET_STATIC_DNS)
  IPAddress ip(ETHERNET_STATIC_IP);
  IPAddress gateway(ETHERNET_STATIC_GATEWAY);
  IPAddress subnet(ETHERNET_STATIC_SUBNET);
  IPAddress dns(ETHERNET_STATIC_DNS);
  Ethernet.begin(mac, ip, dns, gateway, subnet);
  #else
  ETHERNET_DEBUG_PRINTLN("Begin");
  if (Ethernet.begin(mac) == 0) {
    ETHERNET_DEBUG_PRINTLN("Begin failed.");

    // DHCP failed -- let's figure out why
    if (Ethernet.hardwareStatus() == EthernetNoHardware)  // Check for Ethernet hardware present.
    {
      ETHERNET_DEBUG_PRINTLN("Ethernet hardware not found.");
      return false;
    }
    if (Ethernet.linkStatus() == LinkOFF)     // No physical connection
    {
      ETHERNET_DEBUG_PRINTLN("Ethernet cable not connected.");
      return false;
    }
    ETHERNET_DEBUG_PRINTLN("Ethernet: DHCP failed for unknown reason.");
    return false;
  }
  #endif
  ETHERNET_DEBUG_PRINTLN("Ethernet begin complete");
  ETHERNET_DEBUG_PRINT_IP("IP", Ethernet.localIP());
  ETHERNET_DEBUG_PRINT_IP("Subnet", Ethernet.subnetMask());
  ETHERNET_DEBUG_PRINT_IP("Gateway", Ethernet.gatewayIP());

  server.begin();   // start listening for clients
  ETHERNET_DEBUG_PRINTLN("Ethernet: listening on TCP port: %d", ETHERNET_TCP_PORT);

  return true;
}

void SerialEthernetInterface::enable() {
  if (_isEnabled) return;

  _isEnabled = true;
  clearBuffers();
}

void SerialEthernetInterface::disable() {
  _isEnabled = false;
  deviceConnected = false;
  if (client) client.stop();
  clearBuffers();
}

size_t SerialEthernetInterface::writeFrame(const uint8_t src[], size_t len) {
  if (len > MAX_FRAME_SIZE) {
    ETHERNET_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d\n", len);
    return 0;
  }

  if (deviceConnected && len > 0) {
    if (send_queue_len >= FRAME_QUEUE_SIZE) {
      ETHERNET_DEBUG_PRINTLN("writeFrame(), send_queue is full!");
      return 0;
    }

    send_queue[send_queue_len].len = len;  // add to send queue
    memcpy(send_queue[send_queue_len].buf, src, len);
    send_queue_len++;

    return len;
  }
  return 0;
}

bool SerialEthernetInterface::isWriteBusy() const {
  return send_queue_len >= (FRAME_QUEUE_SIZE * 2 / 3);
}

bool SerialEthernetInterface::isReadBusy() const {
  return _state != RECV_STATE_IDLE;
}

bool SerialEthernetInterface::hasPendingIO() const {
  // The W5100S path is polled and has no wake interrupt. Keep servicing it
  // while a client is connected or either direction has queued frame state.
  return deviceConnected || send_queue_len > 0 || _state != RECV_STATE_IDLE;
}

size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) {
  // Use accept() (not available()) so we only see newly-accepted sockets.
  // available() also returns existing connected sockets that have data,
  // which would cause us to treat each inbound packet as a "new client"
  // and stop() the underlying socket — disconnecting the companion.
  auto newClient = server.accept();
  if (newClient) {
    IPAddress new_ip = newClient.remoteIP();
    uint16_t new_port = newClient.remotePort();
    ETHERNET_DEBUG_PRINTLN(
        "New client accepted %u.%u.%u.%u:%u",
        new_ip[0],
        new_ip[1],
        new_ip[2],
        new_ip[3],
        new_port);

    deviceConnected = false;
    if (client) {
      ETHERNET_DEBUG_PRINTLN("Closing previous client");
      client.stop();
    }
    // Queued replies and partial input belong to the previous TCP session.
    clearBuffers();
    client = newClient;
    ETHERNET_DEBUG_PRINTLN("Switched to new client");
  }

  if (client.connected()) {
    if (!deviceConnected) {
      ETHERNET_DEBUG_PRINTLN(
          "Got connection %u.%u.%u.%u:%u",
          client.remoteIP()[0],
          client.remoteIP()[1],
          client.remoteIP()[2],
          client.remoteIP()[3],
          client.remotePort());
      deviceConnected = true;
    }
  } else {
    bool wasConnected = deviceConnected;
    if (wasConnected || send_queue_len > 0 || _state != RECV_STATE_IDLE) {
      deviceConnected = false;
      clearBuffers();
      if (wasConnected) {
        ETHERNET_DEBUG_PRINTLN("Disconnected");
      }
    }
  }

  if (deviceConnected) {
    if (send_queue_len > 0) {   // first, check send queue

      _last_write = millis();
      int len = send_queue[0].len;

#if ETHERNET_RAW_LINE
      ETHERNET_DEBUG_PRINTLN("TX line len=%d", len);
      client.write(send_queue[0].buf, len);
      client.write("\r\n", 2);
#else
      uint8_t pkt[3+len]; // use same header as serial interface so client can delimit frames
      pkt[0] = '>';
      pkt[1] = (len & 0xFF);  // LSB
      pkt[2] = (len >> 8);    // MSB
      memcpy(&pkt[3], send_queue[0].buf, send_queue[0].len);
      ETHERNET_DEBUG_PRINTLN("Sending frame len=%d", len);
      #if ETHERNET_DEBUG_LOGGING && ARDUINO
      ETHERNET_DEBUG_PRINTLN("TX frame len=%d", len);
      #endif
      client.write(pkt, 3 + len);
#endif
      send_queue_len--;
      for (int i = 0; i < send_queue_len; i++) {   // delete top item from queue
        send_queue[i] = send_queue[i + 1];
      }
    } else {
      while (client.available()) {
        int c = client.read();
        if (c < 0) break;

#if ETHERNET_RAW_LINE
        if (c == '\r' || c == '\n') {
          if (_rx_len == 0) {
            continue;
          }
          uint16_t out_len = _rx_len;
          if (out_len > MAX_FRAME_SIZE) {
            out_len = MAX_FRAME_SIZE;
          }
          memcpy(dest, _rx_buf, out_len);
          _rx_len = 0;
          return out_len;
        }
        if (_rx_len < MAX_FRAME_SIZE) {
          _rx_buf[_rx_len] = (uint8_t)c;
          _rx_len++;
        }
#else
        switch (_state) {
          case RECV_STATE_IDLE:
            if (c == '<') {
              _state = RECV_STATE_HDR_FOUND;
            }
            break;
          case RECV_STATE_HDR_FOUND:
            _frame_len = (uint8_t)c;
            _state = RECV_STATE_LEN1_FOUND;
            break;
          case RECV_STATE_LEN1_FOUND:
            _frame_len |= ((uint16_t)c) << 8;
            _rx_len = 0;
            _state = _frame_len > 0 ? RECV_STATE_LEN2_FOUND : RECV_STATE_IDLE;
            break;
          default:
            if (_rx_len < MAX_FRAME_SIZE) {
              _rx_buf[_rx_len] = (uint8_t)c;
            }
            _rx_len++;
            if (_rx_len >= _frame_len) {
              if (_frame_len > MAX_FRAME_SIZE) {
                _frame_len = MAX_FRAME_SIZE;
              }
              #if ETHERNET_DEBUG_LOGGING && ARDUINO
              ETHERNET_DEBUG_PRINTLN("RX frame len=%d", _frame_len);
              #endif
              memcpy(dest, _rx_buf, _frame_len);
              _state = RECV_STATE_IDLE;
              return _frame_len;
            }
        }
#endif
      }
    }
  }

  return 0;
}

bool SerialEthernetInterface::isConnected() const {
  return deviceConnected;
}

void SerialEthernetInterface::loop() {
  Ethernet.maintain();
}

#endif // ETHERNET_ENABLED
