#include "MessageServer.hpp"

#include <stdexcept>
#include <limits>

#include <QNetworkInterface>
#include <QUdpSocket>
#include <QTimer>
#include <QHash>

#include "Radio.hpp"
#include "Network/NetworkMessage.hpp"
#include "qt_helpers.hpp"

#include "pimpl_impl.hpp"

#include "moc_MessageServer.cpp"

namespace
{
  auto quint32_max = std::numeric_limits<quint32>::max ();
}

class MessageServer::impl
  : public QUdpSocket
{
  Q_OBJECT;

public:
  impl (MessageServer * self, QString const& version, QString const& revision)
    : self_ {self}
    , version_ {version}
    , revision_ {revision}
    , clock_ {new QTimer {this}}
  {
    // register the required types with Qt
    Radio::register_types ();

    connect (this, &QIODevice::readyRead, this, &MessageServer::impl::pending_datagrams);
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
    connect (this, static_cast<void (impl::*) (SocketError)> (&impl::error)
             , [this] (SocketError /* e */)
             {
               Q_EMIT self_->error (errorString ());
             });
#else
    connect (this, &impl::errorOccurred, [this] (SocketError /* e */)
                                 {
                                   Q_EMIT self_->error (errorString ());
                                 });
#endif
    connect (clock_, &QTimer::timeout, this, &impl::tick);
    clock_->start (NetworkMessage::pulse * 1000);
  }

  enum StreamStatus {Fail, Short, OK};

  void leave_multicast_group ();
  void join_multicast_group ();
  void parse_message (QHostAddress const& sender, port_type sender_port, QByteArray const& msg);
  void tick ();
  void pending_datagrams ();
  StreamStatus check_status (QDataStream const&) const;
  void send_message (QDataStream const& out, QByteArray const& message, QHostAddress const& address, port_type port)
  {
      if (OK == check_status (out))
        {
          writeDatagram (message, address, port);
        }
      else
        {
          Q_EMIT self_->error ("Error creating UDP message");
        }
  }

  MessageServer * self_;
  QString version_;
  QString revision_;
  QHostAddress multicast_group_address_;
  QSet<QString> network_interfaces_;
  static BindMode constexpr bind_mode_ = ShareAddress | ReuseAddressHint;
  struct Client
  {
    Client () = default;
    Client (port_type const& sender_port)
      : sender_port_ {sender_port}
      , negotiated_schema_number_ {2} // not 1 because it's broken
      , last_activity_ {QDateTime::currentDateTime ()}
    {
    }
    Client (Client const&) = default;
    Client& operator= (Client const&) = default;

    port_type sender_port_;
    quint32 negotiated_schema_number_;
    QDateTime last_activity_;
  };
  QHash<ClientKey, Client> clients_; // maps id to Client
  QTimer * clock_;
};

MessageServer::impl::BindMode constexpr MessageServer::impl::bind_mode_;

#include "MessageServer.moc"

void MessageServer::impl::leave_multicast_group ()
{
  if (BoundState == state () && is_multicast_address (multicast_group_address_))
    {
      for (auto const& if_name : network_interfaces_)
        {
          leaveMulticastGroup (multicast_group_address_, QNetworkInterface::interfaceFromName (if_name));
        }
    }
}

void MessageServer::impl::join_multicast_group ()
{
  if (BoundState == state () && is_multicast_address (multicast_group_address_))
    {
      if (network_interfaces_.size ())
        {
          for (auto const& if_name : network_interfaces_)
            {
              joinMulticastGroup (multicast_group_address_, QNetworkInterface::interfaceFromName (if_name));
            }
        }
      else
        {
          // find the loop-back interface and join on that
          for (auto const& net_if : QNetworkInterface::allInterfaces ())
            {
              auto flags = QNetworkInterface::IsUp | QNetworkInterface::IsLoopBack | QNetworkInterface::CanMulticast;
              if ((net_if.flags () & flags) == flags)
                {
                  joinMulticastGroup (multicast_group_address_, net_if);
                  break;
                }
            }
        }
    }
}

void MessageServer::impl::pending_datagrams ()
{
  while (hasPendingDatagrams ())
    {
      QByteArray datagram;
      datagram.resize (pendingDatagramSize ());
      QHostAddress sender_address;
      port_type sender_port;
      if (0 <= readDatagram (datagram.data (), datagram.size (), &sender_address, &sender_port))
        {
          parse_message (sender_address, sender_port, datagram);
        }
    }
}

void MessageServer::impl::parse_message (QHostAddress const& sender, port_type sender_port, QByteArray const& msg)
{
  try
    {
      //
      // message format is described in NetworkMessage.hpp
      //
      NetworkMessage::Reader in {msg};

      auto id = in.id ();
      if (OK == check_status (in))
        {
          auto client_key = ClientKey {sender, id};
          if (!clients_.contains (client_key))
            {
              auto& client = (clients_[client_key] = {sender_port});
              QByteArray client_version;
              QByteArray client_revision;

              if (NetworkMessage::Heartbeat == in.type ())
                {
                  // negotiate a working schema number
                  in >> client.negotiated_schema_number_;
                  if (OK == check_status (in))
                    {
                      auto sn = NetworkMessage::Builder::schema_number;
                      client.negotiated_schema_number_ = std::min (sn, client.negotiated_schema_number_);

                      // reply to the new client informing it of the
                      // negotiated schema number
                      QByteArray message;
                      NetworkMessage::Builder hb {&message, NetworkMessage::Heartbeat, id, client.negotiated_schema_number_};
                      hb << NetworkMessage::Builder::schema_number // maximum schema number accepted
                         << version_.toUtf8 () << revision_.toUtf8 ();
                      if (impl::OK == check_status (hb))
                        {
                          writeDatagram (message, client_key.first, sender_port);
                        }
                      else
                        {
                          Q_EMIT self_->error ("Error creating UDP message");
                        }
                    }
                  // we don't care if this fails to read
                  in >> client_version >> client_revision;
                }
              Q_EMIT self_->client_opened (client_key, QString::fromUtf8 (client_version),
                                           QString::fromUtf8 (client_revision));
            }
          clients_[client_key].last_activity_ = QDateTime::currentDateTime ();
  
          //
          // message format is described in NetworkMessage.hpp
          //
          switch (in.type ())
            {
            case NetworkMessage::Heartbeat:
              //nothing to do here as time out handling deals with lifetime
              break;

            case NetworkMessage::Clear:
              Q_EMIT self_->decodes_cleared (client_key);
              break;

            case NetworkMessage::Status:
              {
                // unpack message
                Frequency f;
                QByteArray mode;
                QByteArray dx_call;
                QByteArray report;
                QByteArray tx_mode;
                bool tx_enabled {false};
                bool transmitting {false};
                bool decoding {false};
                quint32 rx_df {quint32_max};
                quint32 tx_df {quint32_max};
                QByteArray de_call;
                QByteArray de_grid;
                QByteArray dx_grid;
                bool watchdog_timeout {false};
                QByteArray sub_mode;
                bool fast_mode {false};
                quint8 special_op_mode {0};
                quint32 frequency_tolerance {quint32_max};
                quint32 tr_period {quint32_max};
                QByteArray configuration_name;
                QByteArray tx_message;
                in >> f >> mode >> dx_call >> report >> tx_mode >> tx_enabled >> transmitting >> decoding
                   >> rx_df >> tx_df >> de_call >> de_grid >> dx_grid >> watchdog_timeout >> sub_mode
                   >> fast_mode >> special_op_mode >> frequency_tolerance >> tr_period >> configuration_name
                   >> tx_message;
                if (check_status (in) != Fail)
                  {
                    Q_EMIT self_->status_update (client_key, f, QString::fromUtf8 (mode)
                                                 , QString::fromUtf8 (dx_call)
                                                 , QString::fromUtf8 (report), QString::fromUtf8 (tx_mode)
                                                 , tx_enabled, transmitting, decoding, rx_df, tx_df
                                                 , QString::fromUtf8 (de_call), QString::fromUtf8 (de_grid)
                                                 , QString::fromUtf8 (dx_grid), watchdog_timeout
                                                 , QString::fromUtf8 (sub_mode), fast_mode
                                                 , special_op_mode, frequency_tolerance, tr_period
                                                 , QString::fromUtf8 (configuration_name)
                                                 , QString::fromUtf8 (tx_message));
                  }
              }
              break;

            case NetworkMessage::Decode:
              {
                // unpack message
                bool is_new {true};
                QTime time;
                qint32 snr;
                float delta_time;
                quint32 delta_frequency;
                QByteArray mode;
                QByteArray message;
                bool low_confidence {false};
                bool off_air {false};
                in >> is_new >> time >> snr >> delta_time >> delta_frequency >> mode
                   >> message >> low_confidence >> off_air;
                if (check_status (in) != Fail)
                  {
                    Q_EMIT self_->decode (is_new, client_key, time, snr, delta_time, delta_frequency
                                          , QString::fromUtf8 (mode), QString::fromUtf8 (message)
                                          , low_confidence, off_air);
                  }
              }
              break;

            case NetworkMessage::WSPRDecode:
              {
                // unpack message
                bool is_new {true};
                QTime time;
                qint32 snr;
                float delta_time;
                Frequency frequency;
                qint32 drift;
                QByteArray callsign;
                QByteArray grid;
                qint32 power;
                bool off_air {false};
                in >> is_new >> time >> snr >> delta_time >> frequency >> drift >> callsign >> grid >> power
                   >> off_air;
                if (check_status (in) != Fail)
                  {
                    Q_EMIT self_->WSPR_decode (is_new, client_key, time, snr, delta_time, frequency, drift
                                               , QString::fromUtf8 (callsign), QString::fromUtf8 (grid)
                                               , power, off_air);
                  }
              }
              break;

            case NetworkMessage::QSOLogged:
              {
                QDateTime time_off;
                QByteArray dx_call;
                QByteArray dx_grid;
                Frequency dial_frequency;
                QByteArray mode;
                QByteArray report_sent;
                QByteArray report_received;
                QByteArray tx_power;
                QByteArray comments;
                QByteArray name;
                QDateTime time_on; // Note: LOTW uses TIME_ON for their +/- 30-minute time window
                QByteArray operator_call;
                QByteArray my_call;
                QByteArray my_grid;
                QByteArray exchange_sent;
                QByteArray exchange_rcvd;
                QByteArray prop_mode;
                in >> time_off >> dx_call >> dx_grid >> dial_frequency >> mode >> report_sent >> report_received
                   >> tx_power >> comments >> name >> time_on >> operator_call >> my_call >> my_grid
                   >> exchange_sent >> exchange_rcvd >> prop_mode;
                if (check_status (in) != Fail)
                  {
                    Q_EMIT self_->qso_logged (client_key, time_off, QString::fromUtf8 (dx_call)
                                              , QString::fromUtf8 (dx_grid)
                                              , dial_frequency, QString::fromUtf8 (mode)
                                              , QString::fromUtf8 (report_sent)
                                              , QString::fromUtf8 (report_received), QString::fromUtf8 (tx_power)
                                              , QString::fromUtf8 (comments), QString::fromUtf8 (name), time_on
                                              , QString::fromUtf8 (operator_call), QString::fromUtf8 (my_call)
                                              , QString::fromUtf8 (my_grid), QString::fromUtf8 (exchange_sent)
                                              , QString::fromUtf8 (exchange_rcvd), QString::fromUtf8 (prop_mode));
                  }
              }
              break;

            case NetworkMessage::Close:
              Q_EMIT self_->client_closed (client_key);
              clients_.remove (client_key);
              break;

            case NetworkMessage::LoggedADIF:
              {
                QByteArray ADIF;
                in >> ADIF;
                if (check_status (in) != Fail)
                  {
                    Q_EMIT self_->logged_ADIF (client_key, ADIF);
                  }
              }
              break;

            default:
              // Ignore
              break;
            }
        }
      else
        {
          Q_EMIT self_->error ("MessageServer warning: invalid UDP message received");
        }
    }
  catch (std::exception const& e)
    {
      Q_EMIT self_->error (QString {"MessageServer exception: %1"}.arg (e.what ()));
    }
  catch (...)
    {
      Q_EMIT self_->error ("Unexpected exception in MessageServer");
    }
}

void MessageServer::impl::tick ()
{
  auto now = QDateTime::currentDateTime ();
  auto iter = std::begin (clients_);
  while (iter != std::end (clients_))
    {
      if (now > (*iter).last_activity_.addSecs (NetworkMessage::pulse))
        {
          Q_EMIT self_->decodes_cleared (iter.key ());
          Q_EMIT self_->client_closed (iter.key ());
          iter = clients_.erase (iter); // safe while iterating as doesn't rehash
        }
      else
        {
          ++iter;
        }
    }
}

auto MessageServer::impl::check_status (QDataStream const& stream) const -> StreamStatus
{
  auto stat = stream.status ();
  StreamStatus result {Fail};
  switch (stat)
    {
    case QDataStream::ReadPastEnd:
      result = Short;
      break;

    case QDataStream::ReadCorruptData:
      Q_EMIT self_->error ("Message serialization error: read corrupt data");
      break;

    case QDataStream::WriteFailed:
      Q_EMIT self_->error ("Message serialization error: write error");
      break;

    default:
      result = OK;
      break;
    }
  return result;
}

MessageServer::MessageServer (QObject * parent, QString const& version, QString const& revision)
  : QObject {parent}
  , m_ {this, version, revision}
{
}

void MessageServer::start (port_type port, QHostAddress const& multicast_group_address
                           , QSet<QString> const& network_interface_names)
{
  // qDebug () << "MessageServer::start port:" << port << "multicast addr:" << multicast_group_address.toString () << "network interfaces:" << network_interface_names;
  if (port != m_->localPort ()
      || multicast_group_address != m_->multicast_group_address_
      || network_interface_names != m_->network_interfaces_)
    {
      m_->leave_multicast_group ();
      if (impl::UnconnectedState != m_->state ())
        {
          m_->close ();
        }
      if (!(multicast_group_address.isNull () || is_multicast_address (multicast_group_address)))
        {
          Q_EMIT error ("Invalid multicast group address");
        }
      else if (is_MAC_ambiguous_multicast_address (multicast_group_address))
        {
          Q_EMIT error ("MAC-ambiguous IPv4 multicast group address not supported");
        }
      else
        {
          m_->multicast_group_address_ = multicast_group_address;
          m_->network_interfaces_ = network_interface_names;
          QHostAddress local_addr {is_multicast_address (multicast_group_address)
                                   && impl::IPv4Protocol == multicast_group_address.protocol () ? QHostAddress::AnyIPv4 : QHostAddress::Any};
          if (port && m_->bind (local_addr, port, m_->bind_mode_))
            {
              m_->join_multicast_group ();
            }
        }
    }
}

void MessageServer::clear_decodes (ClientKey const& key, quint8 window)
{
  auto iter = m_->clients_.find (key);
  if (iter != std::end (m_->clients_))
    {
      QByteArray message;
      NetworkMessage::Builder out {&message, NetworkMessage::Clear, key.second, (*iter).negotiated_schema_number_};
      out << window;
      m_->send_message (out, message, key.first, (*iter).sender_port_);
    }
}

void MessageServer::reply (ClientKey const& key, QTime time, qint32 snr, float delta_time
                           , quint32 delta_frequency, QString const& mode
                           , QString const& message_text, bool low_confidence, quint8 modifiers)
{
  auto iter = m_->clients_.find (key);
  if (iter != std::end (m_->clients_))
    {
      QByteArray message;
      NetworkMessage::Builder out {&message, NetworkMessage::Reply, key.second, (*iter).negotiated_schema_number_};
      out << time << snr << delta_time << delta_frequency << mode.toUtf8 ()
          << message_text.toUtf8 () << low_confidence << modifiers;
      m_->send_message (out, message, key.first, (*iter).sender_port_);
    }
}

void MessageServer::replay (ClientKey const& key)
{
  auto iter = m_->clients_.find (key);
  if (iter != std::end (m_->clients_))
    {
      QByteArray message;
      NetworkMessage::Builder out {&message, NetworkMessage::Replay, key.second, (*iter).negotiated_schema_number_};
      m_->send_message (out, message, key.first, (*iter).sender_port_);
    }
}

void MessageServer::close (ClientKey const& key)
{
  auto iter = m_->clients_.find (key);
  if (iter != std::end (m_->clients_))
    {
      QByteArray message;
      NetworkMessage::Builder out {&message, NetworkMessage::Close, key.second, (*iter).negotiated_schema_number_};
      m_->send_message (out, message, key.first, (*iter).sender_port_);
    }
}

void MessageServer::halt_tx (ClientKey const& key, bool auto_only)
{
  auto iter = m_->clients_.find (key);
  if (iter != std::end (m_->clients_))
    {
      QByteArray message;
      NetworkMessage::Builder out {&message, NetworkMessage::HaltTx, key.second, (*iter).negotiated_schema_number_};
      out << auto_only;
      m_->send_message (out, message, key.first, (*iter).sender_port_);
    }
}

void MessageServer::free_text (ClientKey const& key, QString const& text, bool send)
{
  auto iter = m_->clients_.find (key);
  if (iter != std::end (m_->clients_))
    {
      QByteArray message;
      NetworkMessage::Builder out {&message, NetworkMessage::FreeText, key.second, (*iter).negotiated_schema_number_};
      out << text.toUtf8 () << send;
      m_->send_message (out, message, key.first, (*iter).sender_port_);
    }
}

void MessageServer::location (ClientKey const& key, QString const& loc)
{
  auto iter = m_->clients_.find (key);
  if (iter != std::end (m_->clients_))
  {
    QByteArray message;
    NetworkMessage::Builder out {&message, NetworkMessage::Location, key.second, (*iter).negotiated_schema_number_};
    out << loc.toUtf8 ();
    m_->send_message (out, message, key.first, (*iter).sender_port_);
  }
}

void MessageServer::highlight_callsign (ClientKey const& key, QString const& callsign
                                        , QColor const& bg, QColor const& fg, bool last_only)
{
  auto iter = m_->clients_.find (key);
  if (iter != std::end (m_->clients_))
  {
    QByteArray message;
    NetworkMessage::Builder out {&message, NetworkMessage::HighlightCallsign, key.second, (*iter).negotiated_schema_number_};
    out << callsign.toUtf8 () << bg << fg << last_only;
    m_->send_message (out, message, key.first, (*iter).sender_port_);
  }
}

void MessageServer::switch_configuration (ClientKey const& key, QString const& configuration_name)
{
  auto iter = m_->clients_.find (key);
  if (iter != std::end (m_->clients_))
  {
    QByteArray message;
    NetworkMessage::Builder out {&message, NetworkMessage::SwitchConfiguration, key.second, (*iter).negotiated_schema_number_};
    out << configuration_name.toUtf8 ();
    m_->send_message (out, message, key.first, (*iter).sender_port_);
  }
}

void MessageServer::configure (ClientKey const& key, QString const& mode, quint32 frequency_tolerance
                               , QString const& submode, bool fast_mode, quint32 tr_period, quint32 rx_df
                               , QString const& dx_call, QString const& dx_grid, bool generate_messages)
{
  auto iter = m_->clients_.find (key);
  if (iter != std::end (m_->clients_))
  {
    QByteArray message;
    NetworkMessage::Builder out {&message, NetworkMessage::Configure, key.second, (*iter).negotiated_schema_number_};
    out << mode.toUtf8 () << frequency_tolerance << submode.toUtf8 () << fast_mode << tr_period << rx_df
        << dx_call.toUtf8 () << dx_grid.toUtf8 () << generate_messages;
    m_->send_message (out, message, key.first, (*iter).sender_port_);
  }
}