#include "bus.h"

#include "dbus.h"

#include <ranges>
#include <set>

using namespace std;
using namespace dBus;
//--------------------------------------------------------------
/* Default constructor */
Bus::Bus()
{
  // Initialization
  m_startTimestamp = std::chrono::steady_clock::now();

  // Start the monitoring thread
  m_monitoringThread = jthread([&](const stop_token &st)
                               {
    while (!st.stop_requested())
    {
      this_thread::sleep_for(100ms);
      updateMonitoringData();
    } });
}
//--------------------------------------------------------------
/* Get the timestamp of when the bus started */
TimePoint Bus::getStartTimestamp() const
{
  return m_startTimestamp;
}
//--------------------------------------------------------------
/* Subscribe a listener to a specific event type */
void Bus::subscribe(const HashID &eventType, Node *node)
{
  // Sanity check
  if (!node)
    throw runtime_error("invalid node handle");

  std::scoped_lock lock(m_routingTable.mtx);

  // Event type not registered yet, create a new entry with the node
  if (!m_routingTable.nodes.contains(eventType))
  {
    m_routingTable.nodes[eventType] = { node };
    m_routingTable.activeNodesCache.insert(node);
    return;
  }

  // Event type already registered, add the node if not already subscribed
  auto &nodes = m_routingTable.nodes.at(eventType);
  if (std::ranges::find(nodes, node) == nodes.end())
  {
    nodes.push_back(node);
    m_routingTable.activeNodesCache.insert(node);
  }
}
//--------------------------------------------------------------
/* Unsubscribe a listener from a specific event type */
void Bus::unsubscribe(const HashID &eventType, Node *node)
{
  // Sanity check
  if (!node)
    throw runtime_error("invalid node handle");

  std::scoped_lock lock(m_routingTable.mtx);

  // Event type not recorded, nothing to do
  if (!m_routingTable.nodes.contains(eventType))
    return;

  // Event type registered, remove the listener if subscribed
  auto &nodes = m_routingTable.nodes.at(eventType);
  std::erase(nodes, node);

  // Check if the node is still subscribed to any message type before removing it from the active nodes cache
  if (!isSubscribedEverywhere(node))
    m_routingTable.activeNodesCache.erase(node);
}
//--------------------------------------------------------------
/* Unsubscribe a listener from all event types */
void Bus::unsubscribeAll(Node *node)
{
  // Sanity check
  if (!node)
    throw runtime_error("invalid node handle");

  std::scoped_lock lock(m_routingTable.mtx);

  // Iterate through all event types and remove the node from each list
  for (auto &nodeList : m_routingTable.nodes | std::views::values)
  {
    std::erase(nodeList, node);
  }

  // Check if the node is still subscribed to any message type before removing it from the active nodes cache
  if (!isSubscribedEverywhere(node))
    m_routingTable.activeNodesCache.erase(node);
}
//--------------------------------------------------------------
/* Check if a listener is subscribed to a specific event type */
bool Bus::isSubscribed(const HashID &eventType, const Node *node) const
{
  // Sanity check
  if (!node)
    throw runtime_error("invalid node handle");

  std::scoped_lock lock(m_routingTable.mtx);

  // Event type not recorded, node not subscribed
  if (!m_routingTable.nodes.contains(eventType))
    return false;

  // Event type recorded, check if the node is in the list
  const auto &nodes = m_routingTable.nodes.at(eventType);
  return std::ranges::find(nodes, node) != nodes.end();
}
//--------------------------------------------------------------
/* Subscribe a node to receive all messages (broadcast mode) */
void Bus::subscribeToBroadcast(Node *node)
{
  // Sanity check
  if (!node)
    throw runtime_error("invalid node handle");

  std::scoped_lock lock(m_routingTable.mtx);

  // Add the node to the broadcast nodes set
  m_routingTable.broadcastNodes.insert(node);
  m_routingTable.activeNodesCache.insert(node);
}
//--------------------------------------------------------------
/* Unsubscribe a node from broadcast mode */
void Bus::unsubscribeFromBroadcast(Node *node)
{
  // Sanity check
  if (!node)
    throw runtime_error("invalid node handle");

  std::scoped_lock lock(m_routingTable.mtx);

  // Remove the node from the broadcast nodes set
  m_routingTable.broadcastNodes.erase(node);

  // Check if the node is still subscribed to any message type before removing it from the active nodes cache
  if (!isSubscribedEverywhere(node))
    m_routingTable.activeNodesCache.erase(node);
}
//--------------------------------------------------------------
/* Check if a node is subscribed to broadcast mode */
bool Bus::isSubscribedToBroadcast(Node *node) const
{
  // Sanity check
  if (!node)
    throw runtime_error("invalid node handle");

  std::scoped_lock lock(m_routingTable.mtx);

  // Check if the node is in the broadcast nodes set
  return m_routingTable.broadcastNodes.contains(node);
}
//--------------------------------------------------------------
bool Bus::post(const std::shared_ptr<Message> &message)
{
  // Link the message to the bus (set the posted information for the message)
  message->linkToBus(this);

  // Record the incoming message for monitoring purposes
  recordIncoming();

  std::scoped_lock lock(m_routingTable.mtx);

  // Get the list of nodes subscribed to the message type
  set<Node *> recipients;

  // Add nodes subscribed to the specific message type
  if (m_routingTable.nodes.contains(message->getMessageTypeID()))
  {
    const auto &nodeList = m_routingTable.nodes.at(message->getMessageTypeID());
    for (const auto &node : nodeList)
      recipients.insert(node);
  }
  const bool subscribersFound = !recipients.empty();

  // Add broadcast nodes
  recipients.insert(m_routingTable.broadcastNodes.begin(),
                    m_routingTable.broadcastNodes.end());

  // Dispatch the message to all recipients
  for (const auto &node : recipients)
    node->receiveMessageFromBus(message);

  return subscribersFound;
}
//--------------------------------------------------------------
/* Get the current monitoring data of the bus */
Bus::MonitoringData Bus::getMonitoringData() const
{
  std::scoped_lock lock(m_monitoring.mtx);
  return m_monitoring.data;
}
//--------------------------------------------------------------
/* Set the age of the message (to be called by the message when it is destroyed) */
void Bus::setMessageAge(std::chrono::microseconds age)
{
  scoped_lock lock(m_monitoring.mtx);

  m_monitoring.data.maxAge = std::max(m_monitoring.data.maxAge, age);
}
//--------------------------------------------------------------
/* Check if a node is subscribed to any message type (internal, assumes lock is held) */
bool Bus::isSubscribedEverywhere(Node *node) const
{
  // Sanity check
  if (!node)
    throw runtime_error("invalid node handle");

  // Check if the node is still subscribed to any message type
  for (const auto &nodeList : m_routingTable.nodes | std::views::values)
  {
    if (std::ranges::find(nodeList, node) != nodeList.end())
      return true;
  }

  // Check if the node is subscribed to broadcast mode
  if (m_routingTable.broadcastNodes.contains(node))
    return true;

  return false;
}
//--------------------------------------------------------------
/* Incoming messages for monitoring purposes */
void Bus::recordIncoming()
{
  m_monitoring.messageCounter++;
}
//--------------------------------------------------------------
/* Update the monitoring data of the bus */
void Bus::updateMonitoringData()
{
  if (!m_monitoring.frequencyTimer.isElapsed(10s))
    return;

  // Update monitoring data
  MonitoringData monitoringData;
  {
    std::scoped_lock lock(m_routingTable.mtx, m_monitoring.mtx);

    // Update the global message count with the number of messages received in the last interval
    m_monitoring.data.globalMessageCount += m_monitoring.messageCounter;

    // Calculate the frequency of messages being posted to the bus (messages per second)
    const auto duration_s         = static_cast<double>(chrono::duration_cast<chrono::milliseconds>(m_monitoring.frequencyTimer.getElapsed()).count()) / 1000.0;
    m_monitoring.data.frequencyHz = static_cast<double>(m_monitoring.messageCounter) / duration_s;

    // Update the current message count and maximum age of messages on the bus
    m_monitoring.data.currentMessageCount = 0;
    for (const auto &node : m_routingTable.activeNodesCache)
      m_monitoring.data.currentMessageCount += node->getMessageCount();

    // Copy the monitoring data to a local variable for posting the monitoring message after releasing the locks
    monitoringData = m_monitoring.data;

    // Reset all monitoring counters and timers for the next interval
    m_monitoring.data.maxAge    = {};
    m_monitoring.messageCounter = 0;
    m_monitoring.frequencyTimer.reset();
  }

  // Post monitoring message to the bus
// TODO
  //postMonitoringMsg(*this, monitoringData);
}
//--------------------------------------------------------------
