#include "e131.h"
#ifdef USE_NETWORK
#include "e131_addressable_light_effect.h"
#include "esphome/core/log.h"

#include <algorithm>

namespace esphome {
namespace e131 {

static const char *const TAG = "e131";
static const int PORT = 5568;

E131Component::E131Component() {}

E131Component::~E131Component() {
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
  if (this->socket_) {
    this->socket_->close();
  }
#elif defined(USE_SOCKET_IMPL_LWIP_TCP)
  this->udp_.stop();
#endif
}

void E131Component::setup() {
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
  this->socket_ = socket::socket_ip(SOCK_DGRAM, IPPROTO_IP);

  int enable = 1;
  int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
  if (err != 0) {
    ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err);
    // we can still continue
  }
  err = this->socket_->setblocking(false);
  if (err != 0) {
    ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err);
    this->mark_failed();
    return;
  }

  struct sockaddr_storage server;

  socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), PORT);
  if (sl == 0) {
    ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno);
    this->mark_failed();
    return;
  }

  err = this->socket_->bind((struct sockaddr *) &server, sizeof(server));
  if (err != 0) {
    ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno);
    this->mark_failed();
    return;
  }
#elif defined(USE_SOCKET_IMPL_LWIP_TCP)
  if (!this->udp_.begin(PORT)) {
    ESP_LOGW(TAG, "Cannot bind E1.31 to port %d.", PORT);
    this->mark_failed();
    return;
  }
#endif

  join_igmp_groups_();
}

void E131Component::loop() {
  E131Packet packet;
  int universe = 0;
  uint8_t buf[1460];

#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
  ssize_t len = this->socket_->read(buf, sizeof(buf));
  if (len == -1) {
    return;
  }

  if (!this->packet_(buf, (size_t) len, universe, packet)) {
    ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len);
    return;
  }

  if (!this->process_(universe, packet)) {
    ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count);
  }
#elif defined(USE_SOCKET_IMPL_LWIP_TCP)
  while (auto packet_size = this->udp_.parsePacket()) {
    auto len = this->udp_.read(buf, sizeof(buf));
    if (len <= 0)
      continue;

    if (!this->packet_(buf, (size_t) len, universe, packet)) {
      ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len);
      continue;
    }

    if (!this->process_(universe, packet)) {
      ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count);
    }
  }
#endif
}

void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
  if (std::find(light_effects_.begin(), light_effects_.end(), light_effect) != light_effects_.end()) {
    return;
  }

  auto effect_name = light_effect->get_name();
  ESP_LOGD(TAG, "Registering '%.*s' for universes %d-%d.", (int) effect_name.size(), effect_name.c_str(),
           light_effect->get_first_universe(), light_effect->get_last_universe());

  light_effects_.push_back(light_effect);

  for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
    join_(universe);
  }
}

void E131Component::remove_effect(E131AddressableLightEffect *light_effect) {
  auto it = std::find(light_effects_.begin(), light_effects_.end(), light_effect);
  if (it == light_effects_.end()) {
    return;
  }

  auto effect_name = light_effect->get_name();
  ESP_LOGD(TAG, "Unregistering '%.*s' for universes %d-%d.", (int) effect_name.size(), effect_name.c_str(),
           light_effect->get_first_universe(), light_effect->get_last_universe());

  // Swap with last element and pop for O(1) removal (order doesn't matter)
  *it = light_effects_.back();
  light_effects_.pop_back();

  for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
    leave_(universe);
  }
}

bool E131Component::process_(int universe, const E131Packet &packet) {
  bool handled = false;

  ESP_LOGV(TAG, "Received E1.31 packet for %d universe, with %d bytes", universe, packet.count);

  for (auto *light_effect : light_effects_) {
    handled = light_effect->process_(universe, packet) || handled;
  }

  return handled;
}

}  // namespace e131
}  // namespace esphome
#endif
