/*
 * Copyright (c) 2016 Sébastien Deronne
 *
 * SPDX-License-Identifier: GPL-2.0-only
 *
 * Author: Sébastien Deronne <sebastien.deronne@gmail.com>
 */

#include "ns3/command-line.h"
#include "ns3/config.h"
#include "ns3/ht-configuration.h"
#include "ns3/internet-stack-helper.h"
#include "ns3/ipv4-address-helper.h"
#include "ns3/log.h"
#include "ns3/mobility-helper.h"
#include "ns3/on-off-helper.h"
#include "ns3/packet-sink-helper.h"
#include "ns3/packet-sink.h"
#include "ns3/pointer.h"
#include "ns3/qos-txop.h"
#include "ns3/ssid.h"
#include "ns3/string.h"
#include "ns3/udp-client-server-helper.h"
#include "ns3/udp-server.h"
#include "ns3/wifi-mac.h"
#include "ns3/wifi-net-device.h"
#include "ns3/yans-wifi-channel.h"
#include "ns3/yans-wifi-helper.h"

/**
 * @file
 * @ingroup wifi
 *
 * This example shows how to configure mixed networks (i.e. mixed b/g and HT/non-HT) and how are
 * performance in several scenarios.
 *
 * The example compares first g only and mixed b/g cases with various configurations depending on
 * the following parameters:
 * - protection mode that is configured on the AP;
 * - whether short PPDU format is supported by the 802.11b station;
 * - whether short slot time is supported by both the 802.11g station and the AP.
 *
 * The example then compares HT only and mixed HT/non-HT cases.
 *
 * The output results show that the presence of an 802.11b station strongly affects 802.11g
 * performance. Protection mechanisms ensure that the NAV value of 802.11b stations is set correctly
 * in case of 802.11g transmissions. In practice, those protection mechanism add a lot of overhead,
 * resulting in reduced performance. CTS-To-Self introduces less overhead than Rts-Cts, but is not
 * heard by hidden stations (and is thus generally only recommended as a protection mechanism for
 * access points). Since short slot time is disabled once an 802.11b station enters the network,
 * benefits from short slot time are only observed in a g only configuration.
 *
 * The user can also select the payload size and can choose either an UDP or a TCP connection.
 *
 * Example: `./ns3 run "wifi-mixed-network --isUdp=1"`
 */

using namespace ns3;

NS_LOG_COMPONENT_DEFINE("MixedNetwork");

/** Parameters */
struct Parameters
{
    std::string testName;          //!< Test name
    bool enableErpProtection;      //!< True to enable ERP protection
    std::string erpProtectionMode; //!< ERP protection mode
    bool enableShortSlotTime;      //!< True to enable short slot time
    bool enableShortPhyPreamble;   //!< True to enable short PHY preamble
    WifiStandard apType;           //!< Wifi standard for AP
    uint32_t nWifiB;               //!< Number of 802.11b stations
    bool bHasTraffic;              //!< True if 802.11b stations generate traffic
    uint32_t nWifiG;               //!< Number of 802.11g stations
    bool gHasTraffic;              //!< True if 802.11g stations generate traffic
    uint32_t nWifiN;               //!< Number of 802.11n stations
    bool nHasTraffic;              //!< True if 802.11n stations generate traffic
    bool isUdp;                    //!< True to generate UDP traffic
    uint32_t payloadSize;          //!< Payload size in bytes
    Time simulationTime;           //!< Simulation time
};

/** Unnamed namespace, to disambiguate class Experiment. */
namespace
{

/** Experiment runner, based on Parameters */
class Experiment
{
  public:
    Experiment();
    /**
     * Run an experiment with the given parameters
     * @param params the given parameters
     * @return the throughput
     */
    double Run(Parameters params);
};

Experiment::Experiment()
{
}

double
Experiment::Run(Parameters params)
{
    std::string apTypeString;
    if (params.apType == WIFI_STANDARD_80211g)
    {
        apTypeString = "WIFI_STANDARD_80211g";
    }
    else if (params.apType == WIFI_STANDARD_80211n)
    {
        apTypeString = "WIFI_STANDARD_80211n_2_4GHZ";
    }

    std::cout << "Run: " << params.testName
              << "\n\t enableErpProtection=" << params.enableErpProtection
              << "\n\t erpProtectionMode=" << params.erpProtectionMode
              << "\n\t enableShortSlotTime=" << params.enableShortSlotTime
              << "\n\t enableShortPhyPreamble=" << params.enableShortPhyPreamble
              << "\n\t apType=" << apTypeString << "\n\t nWifiB=" << params.nWifiB
              << "\n\t bHasTraffic=" << params.bHasTraffic << "\n\t nWifiG=" << params.nWifiG
              << "\n\t gHasTraffic=" << params.gHasTraffic << "\n\t nWifiN=" << params.nWifiN
              << "\n\t nHasTraffic=" << params.nHasTraffic << std::endl;

    Config::SetDefault("ns3::WifiRemoteStationManager::ErpProtectionMode",
                       StringValue(params.erpProtectionMode));

    double throughput = 0;
    uint32_t nWifiB = params.nWifiB;
    uint32_t nWifiG = params.nWifiG;
    uint32_t nWifiN = params.nWifiN;
    auto simulationTime = params.simulationTime;
    uint32_t payloadSize = params.payloadSize;

    NodeContainer wifiBStaNodes;
    wifiBStaNodes.Create(nWifiB);
    NodeContainer wifiGStaNodes;
    wifiGStaNodes.Create(nWifiG);
    NodeContainer wifiNStaNodes;
    wifiNStaNodes.Create(nWifiN);
    NodeContainer wifiApNode;
    wifiApNode.Create(1);

    YansWifiChannelHelper channel = YansWifiChannelHelper::Default();
    channel.AddPropagationLoss("ns3::RangePropagationLossModel");

    YansWifiPhyHelper phy;
    phy.SetChannel(channel.Create());

    WifiHelper wifi;
    wifi.SetRemoteStationManager("ns3::IdealWifiManager");

    // 802.11b STA
    wifi.SetStandard(WIFI_STANDARD_80211b);

    WifiMacHelper mac;
    Ssid ssid = Ssid("ns-3-ssid");

    mac.SetType("ns3::StaWifiMac",
                "Ssid",
                SsidValue(ssid),
                "ShortSlotTimeSupported",
                BooleanValue(params.enableShortSlotTime));

    // Configure the PHY preamble type: long or short
    phy.Set("ShortPlcpPreambleSupported", BooleanValue(params.enableShortPhyPreamble));

    NetDeviceContainer bStaDevice;
    bStaDevice = wifi.Install(phy, mac, wifiBStaNodes);

    // 802.11b/g STA
    wifi.SetStandard(WIFI_STANDARD_80211g);
    NetDeviceContainer gStaDevice;
    gStaDevice = wifi.Install(phy, mac, wifiGStaNodes);

    // 802.11b/g/n STA
    wifi.SetStandard(WIFI_STANDARD_80211n);
    NetDeviceContainer nStaDevice;
    mac.SetType("ns3::StaWifiMac",
                "Ssid",
                SsidValue(ssid),
                "BE_BlockAckThreshold",
                UintegerValue(2),
                "ShortSlotTimeSupported",
                BooleanValue(params.enableShortSlotTime));
    nStaDevice = wifi.Install(phy, mac, wifiNStaNodes);

    // AP
    NetDeviceContainer apDevice;
    wifi.SetStandard(params.apType);
    mac.SetType("ns3::ApWifiMac",
                "Ssid",
                SsidValue(ssid),
                "EnableBeaconJitter",
                BooleanValue(false),
                "BE_BlockAckThreshold",
                UintegerValue(2),
                "EnableNonErpProtection",
                BooleanValue(params.enableErpProtection),
                "ShortSlotTimeSupported",
                BooleanValue(params.enableShortSlotTime));
    apDevice = wifi.Install(phy, mac, wifiApNode);

    // Set TXOP limit
    if (params.apType == WIFI_STANDARD_80211n)
    {
        Ptr<NetDevice> dev = wifiApNode.Get(0)->GetDevice(0);
        Ptr<WifiNetDevice> wifi_dev = DynamicCast<WifiNetDevice>(dev);
        Ptr<WifiMac> wifi_mac = wifi_dev->GetMac();
        PointerValue ptr;
        wifi_mac->GetAttribute("BE_Txop", ptr);
        Ptr<QosTxop> edca = ptr.Get<QosTxop>();
        edca->SetTxopLimit(MicroSeconds(3008));
    }
    if (nWifiN > 0)
    {
        Ptr<NetDevice> dev = wifiNStaNodes.Get(0)->GetDevice(0);
        Ptr<WifiNetDevice> wifi_dev = DynamicCast<WifiNetDevice>(dev);
        Ptr<WifiMac> wifi_mac = wifi_dev->GetMac();
        PointerValue ptr;
        wifi_mac->GetAttribute("BE_Txop", ptr);
        Ptr<QosTxop> edca = ptr.Get<QosTxop>();
        edca->SetTxopLimit(MicroSeconds(3008));
    }

    Config::Set("/NodeList/*/DeviceList/*/$ns3::WifiNetDevice/Mac/BE_MaxAmpduSize",
                UintegerValue(0)); // Disable A-MPDU

    // Define mobility model
    MobilityHelper mobility;
    Ptr<ListPositionAllocator> positionAlloc = CreateObject<ListPositionAllocator>();

    positionAlloc->Add(Vector(0.0, 0.0, 0.0));
    for (uint32_t i = 0; i < nWifiB; i++)
    {
        positionAlloc->Add(Vector(5.0, 0.0, 0.0));
    }
    for (uint32_t i = 0; i < nWifiG; i++)
    {
        positionAlloc->Add(Vector(0.0, 5.0, 0.0));
    }
    for (uint32_t i = 0; i < nWifiN; i++)
    {
        positionAlloc->Add(Vector(0.0, 0.0, 5.0));
    }

    mobility.SetPositionAllocator(positionAlloc);
    mobility.SetMobilityModel("ns3::ConstantPositionMobilityModel");
    mobility.Install(wifiApNode);
    mobility.Install(wifiBStaNodes);
    mobility.Install(wifiGStaNodes);
    mobility.Install(wifiNStaNodes);

    // Internet stack
    InternetStackHelper stack;
    stack.Install(wifiApNode);
    stack.Install(wifiBStaNodes);
    stack.Install(wifiGStaNodes);
    stack.Install(wifiNStaNodes);

    Ipv4AddressHelper address;
    address.SetBase("192.168.1.0", "255.255.255.0");
    Ipv4InterfaceContainer bStaInterface;
    bStaInterface = address.Assign(bStaDevice);
    Ipv4InterfaceContainer gStaInterface;
    gStaInterface = address.Assign(gStaDevice);
    Ipv4InterfaceContainer nStaInterface;
    nStaInterface = address.Assign(nStaDevice);
    Ipv4InterfaceContainer ApInterface;
    ApInterface = address.Assign(apDevice);

    // Setting applications
    if (params.isUdp)
    {
        uint16_t port = 9;
        UdpServerHelper server(port);
        ApplicationContainer serverApp = server.Install(wifiApNode);
        serverApp.Start(Seconds(0));
        serverApp.Stop(simulationTime + Seconds(1));

        UdpClientHelper client(ApInterface.GetAddress(0), port);
        client.SetAttribute("MaxPackets", UintegerValue(4294967295U));
        client.SetAttribute("Interval", TimeValue(Time("0.0002"))); // packets/s
        client.SetAttribute("PacketSize", UintegerValue(payloadSize));

        ApplicationContainer clientApps;
        if (params.bHasTraffic)
        {
            clientApps.Add(client.Install(wifiBStaNodes));
        }
        if (params.gHasTraffic)
        {
            clientApps.Add(client.Install(wifiGStaNodes));
        }
        if (params.nHasTraffic)
        {
            clientApps.Add(client.Install(wifiNStaNodes));
        }
        clientApps.Start(Seconds(1));
        clientApps.Stop(simulationTime + Seconds(1));

        Simulator::Stop(simulationTime + Seconds(1));
        Simulator::Run();

        double totalPacketsThrough = DynamicCast<UdpServer>(serverApp.Get(0))->GetReceived();
        throughput = totalPacketsThrough * payloadSize * 8 / simulationTime.GetMicroSeconds();
    }
    else
    {
        uint16_t port = 50000;
        Address localAddress(InetSocketAddress(Ipv4Address::GetAny(), port));
        PacketSinkHelper packetSinkHelper("ns3::TcpSocketFactory", localAddress);

        ApplicationContainer serverApp = packetSinkHelper.Install(wifiApNode.Get(0));
        serverApp.Start(Seconds(0));
        serverApp.Stop(simulationTime + Seconds(1));

        OnOffHelper onoff("ns3::TcpSocketFactory", Ipv4Address::GetAny());
        onoff.SetAttribute("OnTime", StringValue("ns3::ConstantRandomVariable[Constant=1]"));
        onoff.SetAttribute("OffTime", StringValue("ns3::ConstantRandomVariable[Constant=0]"));
        onoff.SetAttribute("PacketSize", UintegerValue(payloadSize));
        onoff.SetAttribute("DataRate", DataRateValue(150000000)); // bit/s

        AddressValue remoteAddress(InetSocketAddress(ApInterface.GetAddress(0), port));
        onoff.SetAttribute("Remote", remoteAddress);

        ApplicationContainer clientApps;
        if (params.bHasTraffic)
        {
            clientApps.Add(onoff.Install(wifiBStaNodes));
        }
        if (params.gHasTraffic)
        {
            clientApps.Add(onoff.Install(wifiGStaNodes));
        }
        if (params.nHasTraffic)
        {
            clientApps.Add(onoff.Install(wifiNStaNodes));
        }
        clientApps.Start(Seconds(1));
        clientApps.Stop(simulationTime + Seconds(1));

        Simulator::Stop(simulationTime + Seconds(1));
        Simulator::Run();

        double totalPacketsThrough = DynamicCast<PacketSink>(serverApp.Get(0))->GetTotalRx();
        throughput += totalPacketsThrough * 8 / simulationTime.GetMicroSeconds();
    }
    Simulator::Destroy();
    return throughput;
}

} // unnamed namespace

int
main(int argc, char* argv[])
{
    Parameters params;
    params.testName = "";
    params.enableErpProtection = false;
    params.erpProtectionMode = "Cts-To-Self";
    params.enableShortSlotTime = false;
    params.enableShortPhyPreamble = false;
    params.apType = WIFI_STANDARD_80211g;
    params.nWifiB = 0;
    params.bHasTraffic = false;
    params.nWifiG = 1;
    params.gHasTraffic = true;
    params.nWifiN = 0;
    params.nHasTraffic = false;
    params.isUdp = true;
    params.payloadSize = 1472; // bytes
    params.simulationTime = Seconds(10);

    bool verifyResults = false; // used for regression

    CommandLine cmd(__FILE__);
    cmd.AddValue("payloadSize", "Payload size in bytes", params.payloadSize);
    cmd.AddValue("simulationTime", "Simulation time", params.simulationTime);
    cmd.AddValue("isUdp", "UDP if set to 1, TCP otherwise", params.isUdp);
    cmd.AddValue("verifyResults",
                 "Enable/disable results verification at the end of the simulation",
                 verifyResults);
    cmd.Parse(argc, argv);

    Experiment experiment;
    double throughput = 0;

    params.testName = "g only with all g features disabled";
    throughput = experiment.Run(params);
    if (verifyResults && (throughput < 22.5 || throughput > 23.5))
    {
        NS_LOG_ERROR("Obtained throughput " << throughput << " is not in the expected boundaries!");
        exit(1);
    }
    std::cout << "Throughput: " << throughput << " Mbit/s \n" << std::endl;

    params.testName = "g only with short slot time enabled";
    params.enableErpProtection = false;
    params.enableShortSlotTime = true;
    params.enableShortPhyPreamble = false;
    params.nWifiB = 0;
    throughput = experiment.Run(params);
    if (verifyResults && (throughput < 29 || throughput > 30))
    {
        NS_LOG_ERROR("Obtained throughput " << throughput << " is not in the expected boundaries!");
        exit(1);
    }
    std::cout << "Throughput: " << throughput << " Mbit/s \n" << std::endl;

    params.testName = "Mixed b/g with all g features disabled";
    params.enableErpProtection = false;
    params.enableShortSlotTime = false;
    params.enableShortPhyPreamble = false;
    params.nWifiB = 1;
    throughput = experiment.Run(params);
    if (verifyResults && (throughput < 22.5 || throughput > 23.5))
    {
        NS_LOG_ERROR("Obtained throughput " << throughput << " is not in the expected boundaries!");
        exit(1);
    }
    std::cout << "Throughput: " << throughput << " Mbit/s \n" << std::endl;

    params.testName = "Mixed b/g with short plcp preamble enabled";
    params.enableErpProtection = false;
    params.enableShortSlotTime = false;
    params.enableShortPhyPreamble = true;
    params.nWifiB = 1;
    throughput = experiment.Run(params);
    if (verifyResults && (throughput < 22.5 || throughput > 23.5))
    {
        NS_LOG_ERROR("Obtained throughput " << throughput << " is not in the expected boundaries!");
        exit(1);
    }
    std::cout << "Throughput: " << throughput << " Mbit/s \n" << std::endl;

    params.testName = "Mixed b/g with short slot time enabled using RTS-CTS protection";
    params.enableErpProtection = true;
    params.erpProtectionMode = "Rts-Cts";
    params.enableShortSlotTime = false;
    params.enableShortPhyPreamble = false;
    params.nWifiB = 1;
    throughput = experiment.Run(params);
    if (verifyResults && (throughput < 19 || throughput > 20))
    {
        NS_LOG_ERROR("Obtained throughput " << throughput << " is not in the expected boundaries!");
        exit(1);
    }
    std::cout << "Throughput: " << throughput << " Mbit/s \n" << std::endl;

    params.testName = "Mixed b/g with short plcp preamble enabled using RTS-CTS protection";
    params.enableErpProtection = true;
    params.enableShortSlotTime = false;
    params.enableShortPhyPreamble = true;
    params.nWifiB = 1;
    throughput = experiment.Run(params);
    if (verifyResults && (throughput < 19 || throughput > 20))
    {
        NS_LOG_ERROR("Obtained throughput " << throughput << " is not in the expected boundaries!");
        exit(1);
    }
    std::cout << "Throughput: " << throughput << " Mbit/s \n" << std::endl;

    params.testName = "Mixed b/g with short slot time enabled using CTS-TO-SELF protection";
    params.enableErpProtection = true;
    params.erpProtectionMode = "Cts-To-Self";
    params.enableShortSlotTime = false;
    params.enableShortPhyPreamble = false;
    params.nWifiB = 1;
    throughput = experiment.Run(params);
    if (verifyResults && (throughput < 20.5 || throughput > 21.5))
    {
        NS_LOG_ERROR("Obtained throughput " << throughput << " is not in the expected boundaries!");
        exit(1);
    }
    std::cout << "Throughput: " << throughput << " Mbit/s \n" << std::endl;

    params.testName = "Mixed b/g with short plcp preamble enabled using CTS-TO-SELF protection";
    params.enableErpProtection = true;
    params.enableShortSlotTime = false;
    params.enableShortPhyPreamble = true;
    params.nWifiB = 1;
    throughput = experiment.Run(params);
    if (verifyResults && (throughput < 20.5 || throughput > 21.5))
    {
        NS_LOG_ERROR("Obtained throughput " << throughput << " is not in the expected boundaries!");
        exit(1);
    }
    std::cout << "Throughput: " << throughput << " Mbit/s \n" << std::endl;

    params.testName = "HT only";
    params.enableErpProtection = false;
    params.enableShortSlotTime = false;
    params.enableShortPhyPreamble = false;
    params.apType = WIFI_STANDARD_80211n;
    params.nWifiB = 0;
    params.bHasTraffic = false;
    params.nWifiG = 0;
    params.gHasTraffic = false;
    params.nWifiN = 1;
    params.nHasTraffic = true;
    throughput = experiment.Run(params);
    if (verifyResults && (throughput < 44 || throughput > 45))
    {
        NS_LOG_ERROR("Obtained throughput " << throughput << " is not in the expected boundaries!");
        exit(1);
    }
    std::cout << "Throughput: " << throughput << " Mbit/s \n" << std::endl;

    params.testName = "Mixed HT/non-HT";
    params.enableErpProtection = false;
    params.enableShortSlotTime = false;
    params.enableShortPhyPreamble = false;
    params.apType = WIFI_STANDARD_80211n;
    params.nWifiB = 0;
    params.bHasTraffic = false;
    params.nWifiG = 1;
    params.gHasTraffic = false;
    params.nWifiN = 1;
    params.nHasTraffic = true;
    throughput = experiment.Run(params);
    if (verifyResults && (throughput < 44 || throughput > 45))
    {
        NS_LOG_ERROR("Obtained throughput " << throughput << " is not in the expected boundaries!");
        exit(1);
    }
    std::cout << "Throughput: " << throughput << " Mbit/s \n" << std::endl;

    return 0;
}
