/*
 * Copyright (c) 2014 Universidad de la República - Uruguay
 *
 * SPDX-License-Identifier: GPL-2.0-only
 *
 * Author: Matias Richart <mrichart@fing.edu.uy>
 */

/**
 * This example program is designed to illustrate the behavior of three
 * power/rate-adaptive WiFi rate controls; namely, ns3::ParfWifiManager,
 * ns3::AparfWifiManager and ns3::RrpaaWifiManager.
 *
 * This simulation consist of 4 nodes, two APs and two STAs.
 * The APs generates UDP traffic with a CBR of 54 Mbps to the STAs.
 * The APa use any power and rate control mechanism, and the STAs use only
 * Minstrel rate control.
 * The STAs can be configured to be at any distance from the APs.
 *
 * The objective is to test power and rate control in the links with
 * interference from the other link.
 *
 * The output consists of:
 * - A plot of average throughput vs. time.
 * - A plot of average transmit power vs. time.
 * - Plots for the percentage of time the APs are in each MAC state (IDLE, TX, RX, BUSY)
 * - If enabled, the changes of power and rate to standard output.
 * - If enabled, the average throughput, delay, jitter and tx opportunity for the total simulation
 * time.
 *
 * Example usage:
 * \code{.sh}
 *   ./ns3 run "wifi-power-adaptation-interference --manager=ns3::AparfWifiManager
 * --outputFileName=aparf" \endcode
 *
 * Another example (changing STAs position):
 * \code{.sh}
 *   ./ns3 run "wifi-power-adaptation-interference --manager=ns3::AparfWifiManager
 * --outputFileName=aparf --STA1_x=5 --STA2_x=205" \endcode
 *
 * To enable the log of rate and power changes:
 * \code{.sh}
 *   export NS_LOG=PowerAdaptationInterference=level_info
 * @endcode
 */

#include "ns3/command-line.h"
#include "ns3/config.h"
#include "ns3/double.h"
#include "ns3/flow-monitor-helper.h"
#include "ns3/gnuplot.h"
#include "ns3/internet-stack-helper.h"
#include "ns3/ipv4-address-helper.h"
#include "ns3/ipv4-flow-classifier.h"
#include "ns3/log.h"
#include "ns3/mobility-helper.h"
#include "ns3/on-off-helper.h"
#include "ns3/packet-sink-helper.h"
#include "ns3/ssid.h"
#include "ns3/uinteger.h"
#include "ns3/wifi-mac-header.h"
#include "ns3/wifi-mac.h"
#include "ns3/wifi-net-device.h"
#include "ns3/yans-wifi-channel.h"
#include "ns3/yans-wifi-helper.h"

using namespace ns3;

NS_LOG_COMPONENT_DEFINE("PowerAdaptationInterference");

/// Packet size generated at the AP.
static const uint32_t packetSize = 1420;

/**
 * @brief Class to collect node statistics.
 */
class NodeStatistics
{
  public:
    /**
     * @brief Constructor.
     *
     * @param aps Access points
     * @param stas WiFi Stations.
     */
    NodeStatistics(NetDeviceContainer aps, NetDeviceContainer stas);

    /**
     * @brief Collects the statistics at a given time.
     *
     * @param time Time at which the statistics are collected.
     */
    void CheckStatistics(double time);

    /**
     * @brief Callback called by WifiNetDevice/Phy/PhyTxBegin.
     *
     * @param path The trace path.
     * @param packet The sent packet.
     * @param powerW The Tx power.
     */
    void PhyCallback(std::string path, Ptr<const Packet> packet, double powerW);
    /**
     * @brief Callback called by PacketSink/Rx.
     *
     * @param path The trace path.
     * @param packet The received packet.
     * @param from The sender address.
     */
    void RxCallback(std::string path, Ptr<const Packet> packet, const Address& from);
    /**
     * @brief Callback called by WifiNetDevice/RemoteStationManager/x/PowerChange.
     *
     * @param path The trace path.
     * @param oldPower Old Tx power.
     * @param newPower Actual Tx power.
     * @param dest Destination of the transmission.
     */
    void PowerCallback(std::string path, double oldPower, double newPower, Mac48Address dest);
    /**
     * @brief Callback called by WifiNetDevice/RemoteStationManager/x/RateChange.
     *
     * @param path The trace path.
     * @param oldRate Old rate.
     * @param newRate Actual rate.
     * @param dest Destination of the transmission.
     */
    void RateCallback(std::string path, DataRate oldRate, DataRate newRate, Mac48Address dest);
    /**
     * @brief Callback called by YansWifiPhy/State/State.
     *
     * @param path The trace path.
     * @param init Time when the state started.
     * @param duration Amount of time we've been in (or will be in) the state.
     * @param state The state.
     */
    void StateCallback(std::string path, Time init, Time duration, WifiPhyState state);

    /**
     * @brief Get the Throughput output data
     *
     * @return the Throughput output data.
     */
    Gnuplot2dDataset GetDatafile();
    /**
     * @brief Get the Power output data.
     *
     * @return the Power output data.
     */
    Gnuplot2dDataset GetPowerDatafile();
    /**
     * @brief Get the IDLE state output data.
     *
     * @return the IDLE state output data.
     */
    Gnuplot2dDataset GetIdleDatafile();
    /**
     * @brief Get the BUSY state output data.
     *
     * @return the BUSY state output data.
     */
    Gnuplot2dDataset GetBusyDatafile();
    /**
     * @brief Get the TX state output data.
     *
     * @return the TX state output data.
     */
    Gnuplot2dDataset GetTxDatafile();
    /**
     * @brief Get the RX state output data.
     *
     * @return the RX state output data.
     */
    Gnuplot2dDataset GetRxDatafile();

    /**
     * @brief Get the Busy time.
     *
     * @return the busy time.
     */
    double GetBusyTime() const;

  private:
    /// Time, DataRate pair vector.
    typedef std::vector<std::pair<Time, DataRate>> TxTime;
    /**
     * @brief Setup the WifiPhy object.
     *
     * @param phy The WifiPhy to setup.
     */
    void SetupPhy(Ptr<WifiPhy> phy);
    /**
     * @brief Get the time at which a given datarate has been recorded.
     *
     * @param rate The datarate to search.
     * @return the time.
     */
    Time GetCalcTxTime(DataRate rate);

    std::map<Mac48Address, dBm_u> m_currentPower;   //!< Current Tx power for each sender.
    std::map<Mac48Address, DataRate> m_currentRate; //!< Current Tx rate for each sender.
    uint32_t m_bytesTotal;                          //!< Number of received bytes.
    double m_totalEnergy;                           //!< Energy used.
    double m_totalTime;                             //!< Time spent.
    double busyTime;                                //!< BUSY time.
    double idleTime;                                //!< IDLE time.
    double txTime;                                  //!< TX time.
    double rxTime;                                  //!< RX time.
    double m_totalBusyTime;                         //!< Total time in BUSY state.
    double m_totalIdleTime;                         //!< Total time in IDLE state.
    double m_totalTxTime;                           //!< Total time in TX state.
    double m_totalRxTime;                           //!< Total time in RX state.
    TxTime m_timeTable;                             //!< Time, DataRate table.
    Gnuplot2dDataset m_output;                      //!< Throughput output data.
    Gnuplot2dDataset m_output_power;                //!< Power output data.
    Gnuplot2dDataset m_output_idle;                 //!< IDLE output data.
    Gnuplot2dDataset m_output_busy;                 //!< BUSY output data.
    Gnuplot2dDataset m_output_rx;                   //!< RX output data.
    Gnuplot2dDataset m_output_tx;                   //!< TX output data.
};

NodeStatistics::NodeStatistics(NetDeviceContainer aps, NetDeviceContainer stas)
{
    Ptr<NetDevice> device = aps.Get(0);
    Ptr<WifiNetDevice> wifiDevice = DynamicCast<WifiNetDevice>(device);
    Ptr<WifiPhy> phy = wifiDevice->GetPhy();
    SetupPhy(phy);
    DataRate dataRate = DataRate(phy->GetDefaultMode().GetDataRate(phy->GetChannelWidth()));
    const auto power = phy->GetTxPowerEnd();
    for (uint32_t j = 0; j < stas.GetN(); j++)
    {
        Ptr<NetDevice> staDevice = stas.Get(j);
        Ptr<WifiNetDevice> wifiStaDevice = DynamicCast<WifiNetDevice>(staDevice);
        Mac48Address addr = wifiStaDevice->GetMac()->GetAddress();
        m_currentPower[addr] = power;
        m_currentRate[addr] = dataRate;
    }
    m_currentRate[Mac48Address("ff:ff:ff:ff:ff:ff")] = dataRate;
    m_totalEnergy = 0;
    m_totalTime = 0;
    busyTime = 0;
    idleTime = 0;
    txTime = 0;
    rxTime = 0;
    m_totalBusyTime = 0;
    m_totalIdleTime = 0;
    m_totalTxTime = 0;
    m_totalRxTime = 0;
    m_bytesTotal = 0;
    m_output.SetTitle("Throughput Mbits/s");
    m_output_idle.SetTitle("Idle Time");
    m_output_busy.SetTitle("Busy Time");
    m_output_rx.SetTitle("RX Time");
    m_output_tx.SetTitle("TX Time");
}

void
NodeStatistics::SetupPhy(Ptr<WifiPhy> phy)
{
    for (const auto& mode : phy->GetModeList())
    {
        WifiTxVector txVector;
        txVector.SetMode(mode);
        txVector.SetPreambleType(WIFI_PREAMBLE_LONG);
        txVector.SetChannelWidth(phy->GetChannelWidth());
        DataRate dataRate(mode.GetDataRate(phy->GetChannelWidth()));
        Time time = WifiPhy::CalculateTxDuration(packetSize, txVector, phy->GetPhyBand());
        NS_LOG_DEBUG(mode.GetUniqueName() << " " << time.GetSeconds() << " " << dataRate);
        m_timeTable.emplace_back(time, dataRate);
    }
}

Time
NodeStatistics::GetCalcTxTime(DataRate rate)
{
    for (auto i = m_timeTable.begin(); i != m_timeTable.end(); i++)
    {
        if (rate == i->second)
        {
            return i->first;
        }
    }
    NS_ASSERT(false);
    return Time(0);
}

void
NodeStatistics::PhyCallback(std::string path, Ptr<const Packet> packet, double powerW)
{
    WifiMacHeader head;
    packet->PeekHeader(head);
    Mac48Address dest = head.GetAddr1();

    if (head.GetType() == WIFI_MAC_DATA)
    {
        const auto txTimeSeconds = GetCalcTxTime(m_currentRate[dest]).GetSeconds();
        m_totalEnergy += DbmToW(m_currentPower[dest]) * txTimeSeconds;
        m_totalTime += txTimeSeconds;
    }
}

void
NodeStatistics::PowerCallback(std::string path, double oldPower, double newPower, Mac48Address dest)
{
    m_currentPower[dest] = dBm_u{newPower};
}

void
NodeStatistics::RateCallback(std::string path,
                             DataRate oldRate,
                             DataRate newRate,
                             Mac48Address dest)
{
    m_currentRate[dest] = newRate;
}

void
NodeStatistics::StateCallback(std::string path, Time init, Time duration, WifiPhyState state)
{
    if (state == WifiPhyState::CCA_BUSY)
    {
        busyTime += duration.GetSeconds();
        m_totalBusyTime += duration.GetSeconds();
    }
    else if (state == WifiPhyState::IDLE)
    {
        idleTime += duration.GetSeconds();
        m_totalIdleTime += duration.GetSeconds();
    }
    else if (state == WifiPhyState::TX)
    {
        txTime += duration.GetSeconds();
        m_totalTxTime += duration.GetSeconds();
    }
    else if (state == WifiPhyState::RX)
    {
        rxTime += duration.GetSeconds();
        m_totalRxTime += duration.GetSeconds();
    }
}

void
NodeStatistics::RxCallback(std::string path, Ptr<const Packet> packet, const Address& from)
{
    m_bytesTotal += packet->GetSize();
}

void
NodeStatistics::CheckStatistics(double time)
{
    double mbs = ((m_bytesTotal * 8.0) / (1000000 * time));
    m_bytesTotal = 0;
    double atp = m_totalEnergy / time;
    m_totalEnergy = 0;
    m_totalTime = 0;
    m_output_power.Add((Simulator::Now()).GetSeconds(), atp);
    m_output.Add((Simulator::Now()).GetSeconds(), mbs);

    m_output_idle.Add((Simulator::Now()).GetSeconds(), idleTime * 100);
    m_output_busy.Add((Simulator::Now()).GetSeconds(), busyTime * 100);
    m_output_tx.Add((Simulator::Now()).GetSeconds(), txTime * 100);
    m_output_rx.Add((Simulator::Now()).GetSeconds(), rxTime * 100);
    busyTime = 0;
    idleTime = 0;
    txTime = 0;
    rxTime = 0;

    Simulator::Schedule(Seconds(time), &NodeStatistics::CheckStatistics, this, time);
}

Gnuplot2dDataset
NodeStatistics::GetDatafile()
{
    return m_output;
}

Gnuplot2dDataset
NodeStatistics::GetPowerDatafile()
{
    return m_output_power;
}

Gnuplot2dDataset
NodeStatistics::GetIdleDatafile()
{
    return m_output_idle;
}

Gnuplot2dDataset
NodeStatistics::GetBusyDatafile()
{
    return m_output_busy;
}

Gnuplot2dDataset
NodeStatistics::GetRxDatafile()
{
    return m_output_rx;
}

Gnuplot2dDataset
NodeStatistics::GetTxDatafile()
{
    return m_output_tx;
}

double
NodeStatistics::GetBusyTime() const
{
    return m_totalBusyTime + m_totalRxTime;
}

/**
 * Callback called by WifiNetDevice/RemoteStationManager/x/PowerChange.
 *
 * @param path The trace path.
 * @param oldPower Old Tx power.
 * @param newPower Actual Tx power.
 * @param dest Destination of the transmission.
 */
void
PowerCallback(std::string path, double oldPower, double newPower, Mac48Address dest)
{
    NS_LOG_INFO((Simulator::Now()).GetSeconds()
                << " " << dest << " Old power=" << oldPower << " New power=" << newPower);
}

/**
 * @brief Callback called by WifiNetDevice/RemoteStationManager/x/RateChange.
 *
 * @param path The trace path.
 * @param oldRate Old rate.
 * @param newRate Actual rate.
 * @param dest Destination of the transmission.
 */
void
RateCallback(std::string path, DataRate oldRate, DataRate newRate, Mac48Address dest)
{
    NS_LOG_INFO((Simulator::Now()).GetSeconds()
                << " " << dest << " Old rate=" << oldRate << " New rate=" << newRate);
}

int
main(int argc, char* argv[])
{
    // LogComponentEnable("ConstantRateWifiManager", LOG_LEVEL_FUNCTION);

    dBm_u maxPower{17};
    dBm_u minPower{0};
    uint32_t powerLevels{18};

    uint32_t rtsThreshold{2346};
    std::string manager{"ns3::ParfWifiManager"};
    std::string outputFileName{"parf"};
    int ap1_x{0};
    int ap1_y{0};
    int sta1_x{10};
    int sta1_y{0};
    int ap2_x{200};
    int ap2_y{0};
    int sta2_x{180};
    int sta2_y{0};
    Time simuTime{"100s"};

    CommandLine cmd(__FILE__);
    cmd.AddValue("manager", "PRC Manager", manager);
    cmd.AddValue("rtsThreshold", "RTS threshold", rtsThreshold);
    cmd.AddValue("outputFileName", "Output filename", outputFileName);
    cmd.AddValue("simuTime", "Total simulation time (sec)", simuTime);
    cmd.AddValue("maxPower", "Maximum available transmission level (dbm).", maxPower);
    cmd.AddValue("minPower", "Minimum available transmission level (dbm).", minPower);
    cmd.AddValue("powerLevels",
                 "Number of transmission power levels available between "
                 "TxPowerStart and TxPowerEnd included.",
                 powerLevels);
    cmd.AddValue("AP1_x", "Position of AP1 in x coordinate", ap1_x);
    cmd.AddValue("AP1_y", "Position of AP1 in y coordinate", ap1_y);
    cmd.AddValue("STA1_x", "Position of STA1 in x coordinate", sta1_x);
    cmd.AddValue("STA1_y", "Position of STA1 in y coordinate", sta1_y);
    cmd.AddValue("AP2_x", "Position of AP2 in x coordinate", ap2_x);
    cmd.AddValue("AP2_y", "Position of AP2 in y coordinate", ap2_y);
    cmd.AddValue("STA2_x", "Position of STA2 in x coordinate", sta2_x);
    cmd.AddValue("STA2_y", "Position of STA2 in y coordinate", sta2_y);
    cmd.Parse(argc, argv);

    // Define the APs
    NodeContainer wifiApNodes;
    wifiApNodes.Create(2);

    // Define the STAs
    NodeContainer wifiStaNodes;
    wifiStaNodes.Create(2);

    WifiHelper wifi;
    wifi.SetStandard(WIFI_STANDARD_80211a);
    WifiMacHelper wifiMac;
    YansWifiPhyHelper wifiPhy;
    YansWifiChannelHelper wifiChannel = YansWifiChannelHelper::Default();

    wifiPhy.SetChannel(wifiChannel.Create());

    NetDeviceContainer wifiApDevices;
    NetDeviceContainer wifiStaDevices;
    NetDeviceContainer wifiDevices;

    // Configure the STA nodes
    wifi.SetRemoteStationManager("ns3::AarfWifiManager",
                                 "RtsCtsThreshold",
                                 UintegerValue(rtsThreshold));
    wifiPhy.Set("TxPowerStart", DoubleValue(maxPower));
    wifiPhy.Set("TxPowerEnd", DoubleValue(maxPower));

    Ssid ssid = Ssid("AP0");
    wifiMac.SetType("ns3::StaWifiMac",
                    "Ssid",
                    SsidValue(ssid),
                    "MaxMissedBeacons",
                    UintegerValue(1000));
    wifiStaDevices.Add(wifi.Install(wifiPhy, wifiMac, wifiStaNodes.Get(0)));

    ssid = Ssid("AP1");
    wifiMac.SetType("ns3::StaWifiMac", "Ssid", SsidValue(ssid));
    wifiStaDevices.Add(wifi.Install(wifiPhy, wifiMac, wifiStaNodes.Get(1)));

    // Configure the AP nodes
    wifi.SetRemoteStationManager(manager,
                                 "DefaultTxPowerLevel",
                                 UintegerValue(powerLevels - 1),
                                 "RtsCtsThreshold",
                                 UintegerValue(rtsThreshold));
    wifiPhy.Set("TxPowerStart", DoubleValue(minPower));
    wifiPhy.Set("TxPowerEnd", DoubleValue(maxPower));
    wifiPhy.Set("TxPowerLevels", UintegerValue(powerLevels));

    ssid = Ssid("AP0");
    wifiMac.SetType("ns3::ApWifiMac", "Ssid", SsidValue(ssid));
    wifiApDevices.Add(wifi.Install(wifiPhy, wifiMac, wifiApNodes.Get(0)));

    ssid = Ssid("AP1");
    wifiMac.SetType("ns3::ApWifiMac",
                    "Ssid",
                    SsidValue(ssid),
                    "BeaconInterval",
                    TimeValue(MicroSeconds(103424))); // for avoiding collisions);
    wifiApDevices.Add(wifi.Install(wifiPhy, wifiMac, wifiApNodes.Get(1)));

    wifiDevices.Add(wifiStaDevices);
    wifiDevices.Add(wifiApDevices);

    // Configure the mobility.
    MobilityHelper mobility;
    Ptr<ListPositionAllocator> positionAlloc = CreateObject<ListPositionAllocator>();
    positionAlloc->Add(Vector(ap1_x, ap1_y, 0.0));
    positionAlloc->Add(Vector(sta1_x, sta1_y, 0.0));
    positionAlloc->Add(Vector(ap2_x, ap2_y, 0.0));
    positionAlloc->Add(Vector(sta2_x, sta2_y, 0.0));
    mobility.SetPositionAllocator(positionAlloc);
    mobility.SetMobilityModel("ns3::ConstantPositionMobilityModel");
    mobility.Install(wifiApNodes.Get(0));
    mobility.Install(wifiStaNodes.Get(0));
    mobility.Install(wifiApNodes.Get(1));
    mobility.Install(wifiStaNodes.Get(1));

    // Configure the IP stack
    InternetStackHelper stack;
    stack.Install(wifiApNodes);
    stack.Install(wifiStaNodes);
    Ipv4AddressHelper address;
    address.SetBase("10.1.1.0", "255.255.255.0");
    Ipv4InterfaceContainer i = address.Assign(wifiDevices);
    Ipv4Address sinkAddress = i.GetAddress(0);
    Ipv4Address sinkAddress1 = i.GetAddress(1);
    uint16_t port = 9;

    // Configure the CBR generator
    PacketSinkHelper sink("ns3::UdpSocketFactory", InetSocketAddress(sinkAddress, port));
    ApplicationContainer apps_sink = sink.Install(wifiStaNodes.Get(0));

    OnOffHelper onoff("ns3::UdpSocketFactory", InetSocketAddress(sinkAddress, port));
    onoff.SetConstantRate(DataRate("54Mb/s"), packetSize);
    onoff.SetAttribute("StartTime", TimeValue(Seconds(0)));
    onoff.SetAttribute("StopTime", TimeValue(Seconds(100)));
    ApplicationContainer apps_source = onoff.Install(wifiApNodes.Get(0));

    PacketSinkHelper sink1("ns3::UdpSocketFactory", InetSocketAddress(sinkAddress1, port));
    apps_sink.Add(sink1.Install(wifiStaNodes.Get(1)));

    OnOffHelper onoff1("ns3::UdpSocketFactory", InetSocketAddress(sinkAddress1, port));
    onoff1.SetConstantRate(DataRate("54Mb/s"), packetSize);
    onoff1.SetAttribute("StartTime", TimeValue(Seconds(0)));
    onoff1.SetAttribute("StopTime", TimeValue(Seconds(100)));
    apps_source.Add(onoff1.Install(wifiApNodes.Get(1)));

    apps_sink.Start(Seconds(0.5));
    apps_sink.Stop(simuTime);

    //------------------------------------------------------------
    //-- Setup stats and data collection
    //--------------------------------------------

    // Statistics counters
    NodeStatistics statisticsAp0 = NodeStatistics(wifiApDevices, wifiStaDevices);
    NodeStatistics statisticsAp1 = NodeStatistics(wifiApDevices, wifiStaDevices);

    // Register packet receptions to calculate throughput
    Config::Connect("/NodeList/2/ApplicationList/*/$ns3::PacketSink/Rx",
                    MakeCallback(&NodeStatistics::RxCallback, &statisticsAp0));
    Config::Connect("/NodeList/3/ApplicationList/*/$ns3::PacketSink/Rx",
                    MakeCallback(&NodeStatistics::RxCallback, &statisticsAp1));

    // Register power and rate changes to calculate the Average Transmit Power
    Config::Connect("/NodeList/0/DeviceList/*/$ns3::WifiNetDevice/RemoteStationManager/$" +
                        manager + "/PowerChange",
                    MakeCallback(&NodeStatistics::PowerCallback, &statisticsAp0));
    Config::Connect("/NodeList/0/DeviceList/*/$ns3::WifiNetDevice/RemoteStationManager/$" +
                        manager + "/RateChange",
                    MakeCallback(&NodeStatistics::RateCallback, &statisticsAp0));
    Config::Connect("/NodeList/1/DeviceList/*/$ns3::WifiNetDevice/RemoteStationManager/$" +
                        manager + "/PowerChange",
                    MakeCallback(&NodeStatistics::PowerCallback, &statisticsAp1));
    Config::Connect("/NodeList/1/DeviceList/*/$ns3::WifiNetDevice/RemoteStationManager/$" +
                        manager + "/RateChange",
                    MakeCallback(&NodeStatistics::RateCallback, &statisticsAp1));

    Config::Connect("/NodeList/0/DeviceList/*/$ns3::WifiNetDevice/Phy/PhyTxBegin",
                    MakeCallback(&NodeStatistics::PhyCallback, &statisticsAp0));
    Config::Connect("/NodeList/1/DeviceList/*/$ns3::WifiNetDevice/Phy/PhyTxBegin",
                    MakeCallback(&NodeStatistics::PhyCallback, &statisticsAp1));

    // Register States
    Config::Connect(
        "/NodeList/0/DeviceList/*/$ns3::WifiNetDevice/Phy/$ns3::YansWifiPhy/State/State",
        MakeCallback(&NodeStatistics::StateCallback, &statisticsAp0));
    Config::Connect(
        "/NodeList/1/DeviceList/*/$ns3::WifiNetDevice/Phy/$ns3::YansWifiPhy/State/State",
        MakeCallback(&NodeStatistics::StateCallback, &statisticsAp1));

    statisticsAp0.CheckStatistics(1);
    statisticsAp1.CheckStatistics(1);

    // Callbacks to print every change of power and rate
    Config::Connect("/NodeList/[0-1]/DeviceList/*/$ns3::WifiNetDevice/RemoteStationManager/$" +
                        manager + "/PowerChange",
                    MakeCallback(PowerCallback));
    Config::Connect("/NodeList/[0-1]/DeviceList/*/$ns3::WifiNetDevice/RemoteStationManager/$" +
                        manager + "/RateChange",
                    MakeCallback(RateCallback));

    // Calculate Throughput using Flowmonitor

    FlowMonitorHelper flowmon;
    Ptr<FlowMonitor> monitor = flowmon.InstallAll();

    Simulator::Stop(simuTime);
    Simulator::Run();

    Ptr<Ipv4FlowClassifier> classifier = DynamicCast<Ipv4FlowClassifier>(flowmon.GetClassifier());
    std::map<FlowId, FlowMonitor::FlowStats> stats = monitor->GetFlowStats();
    for (auto i = stats.begin(); i != stats.end(); ++i)
    {
        Ipv4FlowClassifier::FiveTuple t = classifier->FindFlow(i->first);
        if (t.sourceAddress == "10.1.1.3" && t.destinationAddress == "10.1.1.1")
        {
            NS_LOG_INFO("Flow " << i->first << " (" << t.sourceAddress << " -> "
                                << t.destinationAddress << ")\n");
            NS_LOG_INFO("  Tx Bytes:   " << i->second.txBytes << "\n");
            NS_LOG_INFO("  Rx Bytes:   " << i->second.rxBytes << "\n");
            NS_LOG_UNCOND("  Throughput to 10.1.1.1: "
                          << i->second.rxBytes * 8.0 /
                                 (i->second.timeLastRxPacket.GetSeconds() -
                                  i->second.timeFirstTxPacket.GetSeconds()) /
                                 1024 / 1024
                          << " Mbps\n");
            NS_LOG_INFO("  Mean delay:   " << i->second.delaySum.GetSeconds() / i->second.rxPackets
                                           << "\n");
            NS_LOG_INFO("  Mean jitter:   "
                        << i->second.jitterSum.GetSeconds() / (i->second.rxPackets - 1) << "\n");
            NS_LOG_INFO("  Tx Opp: " << 1 - (statisticsAp0.GetBusyTime() / simuTime.GetSeconds()));
        }
        if (t.sourceAddress == "10.1.1.4" && t.destinationAddress == "10.1.1.2")
        {
            NS_LOG_INFO("Flow " << i->first << " (" << t.sourceAddress << " -> "
                                << t.destinationAddress << ")\n");
            NS_LOG_INFO("  Tx Bytes:   " << i->second.txBytes << "\n");
            NS_LOG_INFO("  Rx Bytes:   " << i->second.rxBytes << "\n");
            NS_LOG_UNCOND("  Throughput to 10.1.1.2: "
                          << i->second.rxBytes * 8.0 /
                                 (i->second.timeLastRxPacket.GetSeconds() -
                                  i->second.timeFirstTxPacket.GetSeconds()) /
                                 1024 / 1024
                          << " Mbps\n");
            NS_LOG_INFO("  Mean delay:   " << i->second.delaySum.GetSeconds() / i->second.rxPackets
                                           << "\n");
            NS_LOG_INFO("  Mean jitter:   "
                        << i->second.jitterSum.GetSeconds() / (i->second.rxPackets - 1) << "\n");
            NS_LOG_INFO("  Tx Opp: " << 1 - (statisticsAp1.GetBusyTime() / simuTime.GetSeconds()));
        }
    }

    // Plots for AP0
    std::ofstream outfileTh0("throughput-" + outputFileName + "-0.plt");
    Gnuplot gnuplot = Gnuplot("throughput-" + outputFileName + "-0.eps", "Throughput");
    gnuplot.SetTerminal("post eps color enhanced");
    gnuplot.SetLegend("Time (seconds)", "Throughput (Mb/s)");
    gnuplot.SetTitle("Throughput (AP0 to STA) vs time");
    gnuplot.AddDataset(statisticsAp0.GetDatafile());
    gnuplot.GenerateOutput(outfileTh0);

    if (manager == "ns3::ParfWifiManager" || manager == "ns3::AparfWifiManager" ||
        manager == "ns3::RrpaaWifiManager")
    {
        std::ofstream outfilePower0("power-" + outputFileName + "-0.plt");
        gnuplot = Gnuplot("power-" + outputFileName + "-0.eps", "Average Transmit Power");
        gnuplot.SetTerminal("post eps color enhanced");
        gnuplot.SetLegend("Time (seconds)", "Power (mW)");
        gnuplot.SetTitle("Average transmit power (AP0 to STA) vs time");
        gnuplot.AddDataset(statisticsAp0.GetPowerDatafile());
        gnuplot.GenerateOutput(outfilePower0);
    }

    std::ofstream outfileTx0("tx-" + outputFileName + "-0.plt");
    gnuplot = Gnuplot("tx-" + outputFileName + "-0.eps", "Time in TX State");
    gnuplot.SetTerminal("post eps color enhanced");
    gnuplot.SetLegend("Time (seconds)", "Percent");
    gnuplot.SetTitle("Percentage time AP0 in TX state vs time");
    gnuplot.AddDataset(statisticsAp0.GetTxDatafile());
    gnuplot.GenerateOutput(outfileTx0);

    std::ofstream outfileRx0("rx-" + outputFileName + "-0.plt");
    gnuplot = Gnuplot("rx-" + outputFileName + "-0.eps", "Time in RX State");
    gnuplot.SetTerminal("post eps color enhanced");
    gnuplot.SetLegend("Time (seconds)", "Percent");
    gnuplot.SetTitle("Percentage time AP0 in RX state vs time");
    gnuplot.AddDataset(statisticsAp0.GetRxDatafile());
    gnuplot.GenerateOutput(outfileRx0);

    std::ofstream outfileBusy0("busy-" + outputFileName + "-0.plt");
    gnuplot = Gnuplot("busy-" + outputFileName + "-0.eps", "Time in Busy State");
    gnuplot.SetTerminal("post eps color enhanced");
    gnuplot.SetLegend("Time (seconds)", "Percent");
    gnuplot.SetTitle("Percentage time AP0 in Busy state vs time");
    gnuplot.AddDataset(statisticsAp0.GetBusyDatafile());
    gnuplot.GenerateOutput(outfileBusy0);

    std::ofstream outfileIdle0("idle-" + outputFileName + "-0.plt");
    gnuplot = Gnuplot("idle-" + outputFileName + "-0.eps", "Time in Idle State");
    gnuplot.SetTerminal("post eps color enhanced");
    gnuplot.SetLegend("Time (seconds)", "Percent");
    gnuplot.SetTitle("Percentage time AP0 in Idle state vs time");
    gnuplot.AddDataset(statisticsAp0.GetIdleDatafile());
    gnuplot.GenerateOutput(outfileIdle0);

    // Plots for AP1
    std::ofstream outfileTh1("throughput-" + outputFileName + "-1.plt");
    gnuplot = Gnuplot("throughput-" + outputFileName + "-1.eps", "Throughput");
    gnuplot.SetTerminal("post eps color enhanced");
    gnuplot.SetLegend("Time (seconds)", "Throughput (Mb/s)");
    gnuplot.SetTitle("Throughput (AP1 to STA) vs time");
    gnuplot.AddDataset(statisticsAp1.GetDatafile());
    gnuplot.GenerateOutput(outfileTh1);

    if (manager == "ns3::ParfWifiManager" || manager == "ns3::AparfWifiManager" ||
        manager == "ns3::RrpaaWifiManager")
    {
        std::ofstream outfilePower1("power-" + outputFileName + "-1.plt");
        gnuplot = Gnuplot("power-" + outputFileName + "-1.eps", "Average Transmit Power");
        gnuplot.SetTerminal("post eps color enhanced");
        gnuplot.SetLegend("Time (seconds)", "Power (mW)");
        gnuplot.SetTitle("Average transmit power (AP1 to STA) vs time");
        gnuplot.AddDataset(statisticsAp1.GetPowerDatafile());
        gnuplot.GenerateOutput(outfilePower1);
    }

    std::ofstream outfileTx1("tx-" + outputFileName + "-1.plt");
    gnuplot = Gnuplot("tx-" + outputFileName + "-1.eps", "Time in TX State");
    gnuplot.SetTerminal("post eps color enhanced");
    gnuplot.SetLegend("Time (seconds)", "Percent");
    gnuplot.SetTitle("Percentage time AP1 in TX state vs time");
    gnuplot.AddDataset(statisticsAp1.GetTxDatafile());
    gnuplot.GenerateOutput(outfileTx1);

    std::ofstream outfileRx1("rx-" + outputFileName + "-1.plt");
    gnuplot = Gnuplot("rx-" + outputFileName + "-1.eps", "Time in RX State");
    gnuplot.SetTerminal("post eps color enhanced");
    gnuplot.SetLegend("Time (seconds)", "Percent");
    gnuplot.SetTitle("Percentage time AP1 in RX state vs time");
    gnuplot.AddDataset(statisticsAp1.GetRxDatafile());
    gnuplot.GenerateOutput(outfileRx1);

    std::ofstream outfileBusy1("busy-" + outputFileName + "-1.plt");
    gnuplot = Gnuplot("busy-" + outputFileName + "-1.eps", "Time in Busy State");
    gnuplot.SetTerminal("post eps color enhanced");
    gnuplot.SetLegend("Time (seconds)", "Percent");
    gnuplot.SetTitle("Percentage time AP1 in Busy state vs time");
    gnuplot.AddDataset(statisticsAp1.GetBusyDatafile());
    gnuplot.GenerateOutput(outfileBusy1);

    std::ofstream outfileIdle1("idle-" + outputFileName + "-1.plt");
    gnuplot = Gnuplot("idle-" + outputFileName + "-1.eps", "Time in Idle State");
    gnuplot.SetTerminal("post eps color enhanced");
    gnuplot.SetLegend("Time (seconds)", "Percent");
    gnuplot.SetTitle("Percentage time AP1 in Idle state vs time");
    gnuplot.AddDataset(statisticsAp1.GetIdleDatafile());
    gnuplot.GenerateOutput(outfileIdle1);

    Simulator::Destroy();

    return 0;
}
