/*
 * 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.
 *
 * The output of this is typically two plot files, named throughput-parf.plt
 * (or throughput-aparf.plt, if Aparf is used) and power-parf.plt. If
 * Gnuplot program is available, one can use it to convert the plt file
 * into an eps file, by running:
 * \code{.sh}
 *   gnuplot throughput-parf.plt
 * @endcode
 * Also, to enable logging of rate and power changes to the terminal, set this
 * environment variable:
 * \code{.sh}
 *   export NS_LOG=PowerAdaptationDistance=level_info
 * @endcode
 *
 * This simulation consist of 2 nodes, one AP and one STA.
 * The AP generates UDP traffic with a CBR of 54 Mbps to the STA.
 * The AP can use any power and rate control mechanism and the STA uses
 * only Minstrel rate control.
 * The STA can be configured to move away from (or towards to) the AP.
 * By default, the AP is at coordinate (0,0,0) and the STA starts at
 * coordinate (5,0,0) (meters) and moves away on the x axis by 1 meter every
 * second.
 *
 * The output consists of:
 * - A plot of average throughput vs. distance.
 * - A plot of average transmit power vs. distance.
 * - (if logging is enabled) the changes of power and rate to standard output.
 *
 * The Average Transmit Power is defined as an average of the power
 * consumed per measurement interval, expressed in milliwatts.  The
 * power level for each frame transmission is reported by the simulator,
 * and the energy consumed is obtained by multiplying the power by the
 * frame duration.  At every 'stepTime' (defaulting to 1 second), the
 * total energy for the collection period is divided by the step time
 * and converted from dbm to milliwatt units, and this average is
 * plotted against time.
 *
 * When neither Parf, Aparf or Rrpaa is selected as the rate control, the
 * generation of the plot of average transmit power vs distance is suppressed
 * since the other Wifi rate controls do not support the necessary callbacks
 * for computing the average power.
 *
 * To display all the possible arguments and their defaults:
 * \code{.sh}
 *   ./ns3 run "wifi-power-adaptation-distance --help"
 * @endcode
 *
 * Example usage (selecting Aparf rather than Parf):
 * \code{.sh}
 *   ./ns3 run "wifi-power-adaptation-distance --manager=ns3::AparfWifiManager
 * --outputFileName=aparf" \endcode
 *
 * Another example (moving towards the AP):
 * \code{.sh}
 *   ./ns3 run "wifi-power-adaptation-distance --manager=ns3::AparfWifiManager
 * --outputFileName=aparf --stepsSize=-1 --STA1_x=200" \endcode
 *
 * To enable the log of rate and power changes:
 * \code{.sh}
 *   export NS_LOG=PowerAdaptationDistance=level_info
 * @endcode
 */

#include "ns3/command-line.h"
#include "ns3/config.h"
#include "ns3/double.h"
#include "ns3/gnuplot.h"
#include "ns3/internet-stack-helper.h"
#include "ns3/ipv4-address-helper.h"
#include "ns3/log.h"
#include "ns3/mobility-helper.h"
#include "ns3/mobility-model.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("PowerAdaptationDistance");

/// 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 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 Set the Position of a node.
     *
     * @param node The node.
     * @param position The position.
     */
    void SetPosition(Ptr<Node> node, Vector position);
    /**
     * Move a node.
     * @param node The node.
     * @param stepsSize The step size.
     * @param stepsTime Time on each step.
     */
    void AdvancePosition(Ptr<Node> node, int stepsSize, Time stepsTime);
    /**
     * @brief Get the Position of a node.
     *
     * @param node The node.
     * @return the position of the node.
     */
    Vector GetPosition(Ptr<Node> node);

    /**
     * @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();

  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 on a given state.
    double m_totalEnergy;                           //!< Energy used on a given state.
    double m_totalTime;                             //!< Time spent on a given state.
    TxTime m_timeTable;                             //!< Time, DataRate table.
    Gnuplot2dDataset m_output;                      //!< Throughput output data.
    Gnuplot2dDataset m_output_power;                //!< Power 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;
    m_bytesTotal = 0;
    m_output.SetTitle("Throughput Mbits/s");
    m_output_power.SetTitle("Average Transmit Power");
}

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::RxCallback(std::string path, Ptr<const Packet> packet, const Address& from)
{
    m_bytesTotal += packet->GetSize();
}

void
NodeStatistics::SetPosition(Ptr<Node> node, Vector position)
{
    Ptr<MobilityModel> mobility = node->GetObject<MobilityModel>();
    mobility->SetPosition(position);
}

Vector
NodeStatistics::GetPosition(Ptr<Node> node)
{
    Ptr<MobilityModel> mobility = node->GetObject<MobilityModel>();
    return mobility->GetPosition();
}

void
NodeStatistics::AdvancePosition(Ptr<Node> node, int stepsSize, Time stepsTime)
{
    Vector pos = GetPosition(node);
    double mbs = ((m_bytesTotal * 8.0) / stepsTime.GetMicroSeconds());
    m_bytesTotal = 0;
    double atp = m_totalEnergy / stepsTime.GetSeconds();
    m_totalEnergy = 0;
    m_totalTime = 0;
    m_output_power.Add(pos.x, atp);
    m_output.Add(pos.x, mbs);
    pos.x += stepsSize;
    SetPosition(node, pos);
    NS_LOG_INFO("At time " << Simulator::Now().GetSeconds() << " sec; setting new position to "
                           << pos);
    Simulator::Schedule(stepsTime,
                        &NodeStatistics::AdvancePosition,
                        this,
                        node,
                        stepsSize,
                        stepsTime);
}

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

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

/**
 * 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[])
{
    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{5};
    int sta1_y{0};
    uint32_t steps{200};
    meter_u stepsSize{1};
    Time stepsTime{"1s"};

    CommandLine cmd(__FILE__);
    cmd.AddValue("manager", "PRC Manager", manager);
    cmd.AddValue("rtsThreshold", "RTS threshold", rtsThreshold);
    cmd.AddValue("outputFileName", "Output filename", outputFileName);
    cmd.AddValue("steps", "How many different distances to try", steps);
    cmd.AddValue("stepsTime", "Time on each step", stepsTime);
    cmd.AddValue("stepsSize", "Distance between steps", stepsSize);
    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.Parse(argc, argv);

    if (steps == 0)
    {
        std::cout << "Exiting without running simulation; steps value of 0" << std::endl;
    }

    Time simuTime = (steps + 1) * stepsTime;

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

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

    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 node
    wifi.SetRemoteStationManager("ns3::MinstrelWifiManager",
                                 "RtsCtsThreshold",
                                 UintegerValue(rtsThreshold));
    wifiPhy.Set("TxPowerStart", DoubleValue(maxPower));
    wifiPhy.Set("TxPowerEnd", DoubleValue(maxPower));

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

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

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

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

    // Configure the mobility.
    MobilityHelper mobility;
    Ptr<ListPositionAllocator> positionAlloc = CreateObject<ListPositionAllocator>();
    // Initial position of AP and STA
    positionAlloc->Add(Vector(ap1_x, ap1_y, 0.0));
    NS_LOG_INFO("Setting initial AP position to " << Vector(ap1_x, ap1_y, 0.0));
    positionAlloc->Add(Vector(sta1_x, sta1_y, 0.0));
    NS_LOG_INFO("Setting initial STA position to " << Vector(sta1_x, sta1_y, 0.0));
    mobility.SetPositionAllocator(positionAlloc);
    mobility.SetMobilityModel("ns3::ConstantPositionMobilityModel");
    mobility.Install(wifiApNodes.Get(0));
    mobility.Install(wifiStaNodes.Get(0));

    // Statistics counter
    NodeStatistics statistics = NodeStatistics(wifiApDevices, wifiStaDevices);

    // Move the STA by stepsSize meters every stepsTime seconds
    Simulator::Schedule(Seconds(0.5) + stepsTime,
                        &NodeStatistics::AdvancePosition,
                        &statistics,
                        wifiStaNodes.Get(0),
                        stepsSize,
                        stepsTime);

    // 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);
    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.5)));
    onoff.SetAttribute("StopTime", TimeValue(simuTime));
    ApplicationContainer apps_source = onoff.Install(wifiApNodes.Get(0));

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

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

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

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

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

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

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

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

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

    Simulator::Destroy();

    return 0;
}
