#include "qsacnnode.h"

#include <QDebug>
#include <QMetaEnum>
#include <QNetworkDatagram>
#include <QNetworkInterface>


/**
 * @brief QSacnNode::QSacnNode
 * @param parent
 * @param cid
 */
QSacnNode::QSacnNode(QObject *parent, QUuid cid)
    : Component(UUID::uuid(cid.toString().toStdString()))
    , QUdpSocket(parent)
{
    fctn_ = "OpenLCP QSacnNode";

    bind(QHostAddress::AnyIPv4, sACN::ACN_SDT_MULTICAST_PORT);

    connect(this, &QUdpSocket::readyRead,
            this, &QSacnNode::udpReceive);

    sACN::Receiver::onDiscovered([this](){ emit discoveryUpdates(); });
}


/**
 * @brief QSacnNode::~QSacnNode
 */
QSacnNode::~QSacnNode()
{
  auto rx_list = rx_universes.keys();
  for (const auto & num : rx_list)
    QSacnNode::unsubscribe(num);

  auto tx_list = tx_universes.keys();
  for (const auto & num : tx_list)
    QSacnNode::terminate(num);
}


/**
 * @brief QSacnNode::subscribe
 * @param num
 */
void QSacnNode::subscribe(const uint16_t num)
{
  if (Receiver::universe(num))  // already subscribed
    return;
  if (num != sACN::E131_DISCOVERY_UNIVERSE)
    {
      qDebug() << "Subscribing to universe" << QString::number(num);
      Receiver::subscribe(num);
      rx_universes.emplace(num, new QSacnUniverse(this, Receiver::universe(num)));
      emit subscribing(rx_universes.value(num));
    }
  for (QNetworkInterface &iface : QNetworkInterface::allInterfaces())
    joinMulticastGroup(IPv4MulticastAddress(num), iface);
  for (QNetworkInterface &iface : QNetworkInterface::allInterfaces())
    joinMulticastGroup(IPv6MulticastAddress(num), iface);
}


/**
 * @brief QSacnNode::unsubscribe
 * @param num
 */
void QSacnNode::unsubscribe(const uint16_t num)
{
  for (QNetworkInterface &iface : QNetworkInterface::allInterfaces())
    leaveMulticastGroup(IPv4MulticastAddress(num), iface);
  for (QNetworkInterface &iface : QNetworkInterface::allInterfaces())
    leaveMulticastGroup(IPv6MulticastAddress(num), iface);

  if (Receiver::universe(num)) {
    qDebug() << "Unsubscribing from universe " << QString::number(num);
    emit unsubscribing(rx_universes.value(num));
    rx_universes.take(num)->deleteLater();
    Receiver::unsubscribe(num);
  }
}


/**
 * @brief QSacnNode::create
 * @param num
 */
void QSacnNode::create(const uint16_t num)
{
  if (Source::universe(num))  // already created
    return;

  Source::create(num);
  tx_universes.emplace(num, new QSacnUniverse(this, Source::universe(num)));
  emit creating(tx_universes.value(num));
}


/**
 * @brief QSacnNode::terminate
 * @param num
 */
void QSacnNode::terminate(const uint16_t num)
{
  if (Source::universe(num)) {
    qDebug() << "Terminating universe " << QString::number(num);
    Source::terminate(num);
    emit terminating(tx_universes.value(num));
    tx_universes.take(num)->deleteLater();
  }
}

/**
 * @brief QSacnNode::UdpPayloadReceiver
 */
void QSacnNode::udpReceive()
{
    while (hasPendingDatagrams()) {
        QNetworkDatagram datagram = receiveDatagram();
        // Expecting IANA registered Session Data Transport traffic
        if (datagram.destinationPort() != sACN::ACN_SDT_MULTICAST_PORT)
          return;

        // wrap a PDU io stream around the QNetworkDatagram data buffer
        ACN::PDU::Stream stream(new ACN::PDU::pdu_stream(
                                   (uint8_t*)datagram.data().constData(),
                                   datagram.data().length()));
        UdpPayloadReceiver(stream);
    }
}


/**
 * @brief QSacnNode::rlpSend
 * @param stream
 * @param ip
 */
void QSacnNode::rlpSend(const ACN::PDU::Stream stream,
                        const ACN::SDT::UDP::ipAddress& ip)
{
  QHostAddress addr;
  switch (ip.type) {
    case ACN::SDT::SDT_ADDR_IPV4:
      addr = QHostAddress(ip.address.ipv4.value);
      break;
    case ACN::SDT::SDT_ADDR_IPV6:
      addr = QHostAddress(ip.address.ipv6.bytes);
      break;
    default:
      return;
    }

  writeDatagram(reinterpret_cast<char*>(stream->base()), stream->size(),
                addr, ip.port);
}


/**
 * @brief QSacnNode::universe
 * @param u
 * @return
 */
QSacnUniverse* QSacnNode::universe(const uint16_t u)
{
  return rx_universes.value(u);
}


/**
 * @brief QSacnNode::source
 * @param u
 * @return
 */
QSacnUniverse *QSacnNode::source(const uint16_t u)
{
  return tx_universes.value(u);
}
