/*
 * Copyright (c) 2014 Piotr Gawlowicz
 *
 * SPDX-License-Identifier: GPL-2.0-only
 *
 * Author: Piotr Gawlowicz <gawlowicz.p@gmail.com>
 *
 */

#include "ns3/applications-module.h"
#include "ns3/buildings-helper.h"
#include "ns3/core-module.h"
#include "ns3/internet-module.h"
#include "ns3/log.h"
#include "ns3/lte-module.h"
#include "ns3/mobility-module.h"
#include "ns3/network-module.h"
#include "ns3/point-to-point-epc-helper.h"
#include "ns3/point-to-point-module.h"
#include "ns3/spectrum-module.h"

using namespace ns3;

NS_LOG_COMPONENT_DEFINE("LenaDistributedFrequencyReuse");

void
PrintGnuplottableUeListToFile(std::string filename)
{
    std::ofstream outFile;
    outFile.open(filename, std::ios_base::out | std::ios_base::trunc);
    if (!outFile.is_open())
    {
        NS_LOG_ERROR("Can't open file " << filename);
        return;
    }
    for (auto it = NodeList::Begin(); it != NodeList::End(); ++it)
    {
        Ptr<Node> node = *it;
        int nDevs = node->GetNDevices();
        for (int j = 0; j < nDevs; j++)
        {
            Ptr<LteUeNetDevice> uedev = node->GetDevice(j)->GetObject<LteUeNetDevice>();
            if (uedev)
            {
                Vector pos = node->GetObject<MobilityModel>()->GetPosition();
                outFile << "set label \"" << uedev->GetImsi() << "\" at " << pos.x << "," << pos.y
                        << " left font \"Helvetica,4\" textcolor rgb \"grey\" front point pt 1 ps "
                           "0.3 lc rgb \"grey\" offset 0,0"
                        << std::endl;
            }
        }
    }
}

void
PrintGnuplottableEnbListToFile(std::string filename)
{
    std::ofstream outFile;
    outFile.open(filename, std::ios_base::out | std::ios_base::trunc);
    if (!outFile.is_open())
    {
        NS_LOG_ERROR("Can't open file " << filename);
        return;
    }
    for (auto it = NodeList::Begin(); it != NodeList::End(); ++it)
    {
        Ptr<Node> node = *it;
        int nDevs = node->GetNDevices();
        for (int j = 0; j < nDevs; j++)
        {
            Ptr<LteEnbNetDevice> enbdev = node->GetDevice(j)->GetObject<LteEnbNetDevice>();
            if (enbdev)
            {
                Vector pos = node->GetObject<MobilityModel>()->GetPosition();
                outFile << "set label \"" << enbdev->GetCellId() << "\" at " << pos.x << ","
                        << pos.y
                        << " left font \"Helvetica,4\" textcolor rgb \"white\" front  point pt 2 "
                           "ps 0.3 lc rgb \"white\" offset 0,0"
                        << std::endl;
            }
        }
    }
}

int
main(int argc, char* argv[])
{
    Config::SetDefault("ns3::LteSpectrumPhy::CtrlErrorModelEnabled", BooleanValue(true));
    Config::SetDefault("ns3::LteSpectrumPhy::DataErrorModelEnabled", BooleanValue(true));
    Config::SetDefault("ns3::LteHelper::UseIdealRrc", BooleanValue(true));
    Config::SetDefault("ns3::LteHelper::UsePdschForCqiGeneration", BooleanValue(true));

    // Uplink Power Control
    Config::SetDefault("ns3::LteUePhy::EnableUplinkPowerControl", BooleanValue(true));
    Config::SetDefault("ns3::LteUePowerControl::ClosedLoop", BooleanValue(true));
    Config::SetDefault("ns3::LteUePowerControl::AccumulationEnabled", BooleanValue(false));

    uint32_t runId = 3;
    uint16_t numberOfRandomUes = 0;
    double simTime = 5.000;
    bool generateSpectrumTrace = false;
    bool generateRem = false;
    int32_t remRbId = -1;
    uint16_t bandwidth = 25;
    double distance = 1000;
    Box macroUeBox =
        Box(-distance * 0.5, distance * 1.5, -distance * 0.5, distance * 1.5, 1.5, 1.5);

    // Command line arguments
    CommandLine cmd(__FILE__);
    cmd.AddValue("numberOfUes", "Number of UEs", numberOfRandomUes);
    cmd.AddValue("simTime", "Total duration of the simulation (in seconds)", simTime);
    cmd.AddValue("generateSpectrumTrace",
                 "if true, will generate a Spectrum Analyzer trace",
                 generateSpectrumTrace);
    cmd.AddValue("generateRem",
                 "if true, will generate a REM and then abort the simulation",
                 generateRem);
    cmd.AddValue("remRbId",
                 "Resource block Id, for which REM will be generated,"
                 "default value is -1, what means REM will be averaged from all RBs",
                 remRbId);
    cmd.AddValue("runId", "runId", runId);
    cmd.Parse(argc, argv);

    RngSeedManager::SetSeed(1);
    RngSeedManager::SetRun(runId);

    Ptr<LteHelper> lteHelper = CreateObject<LteHelper>();
    Ptr<PointToPointEpcHelper> epcHelper = CreateObject<PointToPointEpcHelper>();
    lteHelper->SetEpcHelper(epcHelper);
    lteHelper->SetHandoverAlgorithmType("ns3::NoOpHandoverAlgorithm"); // disable automatic handover

    Ptr<Node> pgw = epcHelper->GetPgwNode();

    // Create a single RemoteHost
    NodeContainer remoteHostContainer;
    remoteHostContainer.Create(1);
    Ptr<Node> remoteHost = remoteHostContainer.Get(0);
    InternetStackHelper internet;
    internet.Install(remoteHostContainer);

    // Create the Internet
    PointToPointHelper p2ph;
    p2ph.SetDeviceAttribute("DataRate", DataRateValue(DataRate("100Gb/s")));
    p2ph.SetDeviceAttribute("Mtu", UintegerValue(1500));
    p2ph.SetChannelAttribute("Delay", TimeValue(Seconds(0.010)));
    NetDeviceContainer internetDevices = p2ph.Install(pgw, remoteHost);
    Ipv4AddressHelper ipv4h;
    ipv4h.SetBase("1.0.0.0", "255.0.0.0");
    Ipv4InterfaceContainer internetIpIfaces = ipv4h.Assign(internetDevices);
    Ipv4Address remoteHostAddr = internetIpIfaces.GetAddress(1);

    // Routing of the Internet Host (towards the LTE network)
    Ipv4StaticRoutingHelper ipv4RoutingHelper;
    Ptr<Ipv4StaticRouting> remoteHostStaticRouting =
        ipv4RoutingHelper.GetStaticRouting(remoteHost->GetObject<Ipv4>());
    // interface 0 is localhost, 1 is the p2p device
    remoteHostStaticRouting->AddNetworkRouteTo(Ipv4Address("7.0.0.0"), Ipv4Mask("255.0.0.0"), 1);

    // Create Nodes: eNodeB and UE
    NodeContainer enbNodes;
    NodeContainer randomUeNodes;
    enbNodes.Create(3);
    randomUeNodes.Create(numberOfRandomUes);

    /*   the topology is the following:
     *                 eNB3
     *                /     \
     *               /       \
     *              /         \
     *             /           \
     *   distance /             \ distance
     *           /      UEs      \
     *          /                 \
     *         /                   \
     *        /                     \
     *       /                       \
     *   eNB1-------------------------eNB2
     *                  distance
     */

    // Install Mobility Model
    Ptr<ListPositionAllocator> enbPositionAlloc = CreateObject<ListPositionAllocator>();
    enbPositionAlloc->Add(Vector(0.0, 0.0, 0.0));                         // eNB1
    enbPositionAlloc->Add(Vector(distance, 0.0, 0.0));                    // eNB2
    enbPositionAlloc->Add(Vector(distance * 0.5, distance * 0.866, 0.0)); // eNB3
    MobilityHelper mobility;
    mobility.SetMobilityModel("ns3::ConstantPositionMobilityModel");
    mobility.SetPositionAllocator(enbPositionAlloc);
    mobility.Install(enbNodes);

    Ptr<RandomBoxPositionAllocator> randomUePositionAlloc =
        CreateObject<RandomBoxPositionAllocator>();
    Ptr<UniformRandomVariable> xVal = CreateObject<UniformRandomVariable>();
    xVal->SetAttribute("Min", DoubleValue(macroUeBox.xMin));
    xVal->SetAttribute("Max", DoubleValue(macroUeBox.xMax));
    randomUePositionAlloc->SetAttribute("X", PointerValue(xVal));
    Ptr<UniformRandomVariable> yVal = CreateObject<UniformRandomVariable>();
    yVal->SetAttribute("Min", DoubleValue(macroUeBox.yMin));
    yVal->SetAttribute("Max", DoubleValue(macroUeBox.yMax));
    randomUePositionAlloc->SetAttribute("Y", PointerValue(yVal));
    Ptr<UniformRandomVariable> zVal = CreateObject<UniformRandomVariable>();
    zVal->SetAttribute("Min", DoubleValue(macroUeBox.zMin));
    zVal->SetAttribute("Max", DoubleValue(macroUeBox.zMax));
    randomUePositionAlloc->SetAttribute("Z", PointerValue(zVal));
    mobility.SetPositionAllocator(randomUePositionAlloc);
    mobility.Install(randomUeNodes);

    // Create Devices and install them in the Nodes (eNB and UE)
    NetDeviceContainer enbDevs;
    NetDeviceContainer randomUeDevs;
    lteHelper->SetSchedulerType("ns3::PfFfMacScheduler");
    lteHelper->SetSchedulerAttribute("HarqEnabled", BooleanValue(true));

    lteHelper->SetEnbDeviceAttribute("DlBandwidth", UintegerValue(bandwidth));
    lteHelper->SetEnbDeviceAttribute("UlBandwidth", UintegerValue(bandwidth));

    lteHelper->SetFfrAlgorithmType("ns3::LteFfrDistributedAlgorithm");
    lteHelper->SetFfrAlgorithmAttribute("CalculationInterval", TimeValue(MilliSeconds(200)));
    lteHelper->SetFfrAlgorithmAttribute("RsrpDifferenceThreshold", UintegerValue(5));
    lteHelper->SetFfrAlgorithmAttribute("RsrqThreshold", UintegerValue(25));
    lteHelper->SetFfrAlgorithmAttribute("EdgeRbNum", UintegerValue(6));
    lteHelper->SetFfrAlgorithmAttribute("CenterPowerOffset",
                                        UintegerValue(LteRrcSap::PdschConfigDedicated::dB_3));
    lteHelper->SetFfrAlgorithmAttribute("EdgePowerOffset",
                                        UintegerValue(LteRrcSap::PdschConfigDedicated::dB3));

    lteHelper->SetFfrAlgorithmAttribute("CenterAreaTpc", UintegerValue(0));
    lteHelper->SetFfrAlgorithmAttribute("EdgeAreaTpc", UintegerValue(3));

    // ns3::LteFfrDistributedAlgorithm works with Absolute Mode Uplink Power Control
    Config::SetDefault("ns3::LteUePowerControl::AccumulationEnabled", BooleanValue(false));

    enbDevs = lteHelper->InstallEnbDevice(enbNodes);
    randomUeDevs = lteHelper->InstallUeDevice(randomUeNodes);

    // Add X2 interface
    lteHelper->AddX2Interface(enbNodes);

    NodeContainer ueNodes;
    ueNodes.Add(randomUeNodes);
    NetDeviceContainer ueDevs;
    ueDevs.Add(randomUeDevs);

    // Install the IP stack on the UEs
    internet.Install(ueNodes);
    Ipv4InterfaceContainer ueIpIfaces;
    ueIpIfaces = epcHelper->AssignUeIpv4Address(NetDeviceContainer(ueDevs));

    // Attach a UE to a eNB
    lteHelper->AttachToClosestEnb(ueDevs, enbDevs);

    // Install and start applications on UEs and remote host
    uint16_t dlPort = 10000;
    uint16_t ulPort = 20000;

    // randomize a bit start times to avoid simulation artifacts
    // (e.g., buffer overflows due to packet transmissions happening
    // exactly at the same time)
    Ptr<UniformRandomVariable> startTimeSeconds = CreateObject<UniformRandomVariable>();
    startTimeSeconds->SetAttribute("Min", DoubleValue(0));
    startTimeSeconds->SetAttribute("Max", DoubleValue(0.010));

    for (uint32_t u = 0; u < ueNodes.GetN(); ++u)
    {
        Ptr<Node> ue = ueNodes.Get(u);
        // Set the default gateway for the UE
        Ptr<Ipv4StaticRouting> ueStaticRouting =
            ipv4RoutingHelper.GetStaticRouting(ue->GetObject<Ipv4>());
        ueStaticRouting->SetDefaultRoute(epcHelper->GetUeDefaultGatewayAddress(), 1);

        for (uint32_t b = 0; b < 1; ++b)
        {
            ++dlPort;
            ++ulPort;

            ApplicationContainer clientApps;
            ApplicationContainer serverApps;

            UdpClientHelper dlClientHelper(ueIpIfaces.GetAddress(u), dlPort);
            dlClientHelper.SetAttribute("MaxPackets", UintegerValue(1000000));
            dlClientHelper.SetAttribute("Interval", TimeValue(MilliSeconds(1)));
            clientApps.Add(dlClientHelper.Install(remoteHost));
            PacketSinkHelper dlPacketSinkHelper("ns3::UdpSocketFactory",
                                                InetSocketAddress(Ipv4Address::GetAny(), dlPort));
            serverApps.Add(dlPacketSinkHelper.Install(ue));

            UdpClientHelper ulClientHelper(remoteHostAddr, ulPort);
            ulClientHelper.SetAttribute("MaxPackets", UintegerValue(1000000));
            ulClientHelper.SetAttribute("Interval", TimeValue(MilliSeconds(1)));
            clientApps.Add(ulClientHelper.Install(ue));
            PacketSinkHelper ulPacketSinkHelper("ns3::UdpSocketFactory",
                                                InetSocketAddress(Ipv4Address::GetAny(), ulPort));
            serverApps.Add(ulPacketSinkHelper.Install(remoteHost));

            Ptr<EpcTft> tft = Create<EpcTft>();
            EpcTft::PacketFilter dlpf;
            dlpf.localPortStart = dlPort;
            dlpf.localPortEnd = dlPort;
            tft->Add(dlpf);
            EpcTft::PacketFilter ulpf;
            ulpf.remotePortStart = ulPort;
            ulpf.remotePortEnd = ulPort;
            tft->Add(ulpf);
            EpsBearer bearer(EpsBearer::NGBR_VIDEO_TCP_DEFAULT);
            lteHelper->ActivateDedicatedEpsBearer(ueDevs.Get(u), bearer, tft);

            Time startTime = Seconds(startTimeSeconds->GetValue());
            serverApps.Start(startTime);
            clientApps.Start(startTime);
        }
    }

    // Spectrum analyzer
    NodeContainer spectrumAnalyzerNodes;
    spectrumAnalyzerNodes.Create(1);
    SpectrumAnalyzerHelper spectrumAnalyzerHelper;

    if (generateSpectrumTrace)
    {
        Ptr<ListPositionAllocator> positionAlloc = CreateObject<ListPositionAllocator>();
        // position of Spectrum Analyzer
        positionAlloc->Add(Vector(0.0, 0.0, 0.0)); // eNB1
        //      positionAlloc->Add (Vector (distance,  0.0, 0.0));                        // eNB2
        //      positionAlloc->Add (Vector (distance*0.5, distance*0.866, 0.0));          // eNB3

        MobilityHelper mobility;
        mobility.SetMobilityModel("ns3::ConstantPositionMobilityModel");
        mobility.SetPositionAllocator(positionAlloc);
        mobility.Install(spectrumAnalyzerNodes);

        Ptr<LteSpectrumPhy> enbDlSpectrumPhy = enbDevs.Get(0)
                                                   ->GetObject<LteEnbNetDevice>()
                                                   ->GetPhy()
                                                   ->GetDownlinkSpectrumPhy()
                                                   ->GetObject<LteSpectrumPhy>();
        Ptr<SpectrumChannel> dlChannel = enbDlSpectrumPhy->GetChannel();

        spectrumAnalyzerHelper.SetChannel(dlChannel);
        Ptr<SpectrumModel> sm = LteSpectrumValueHelper::GetSpectrumModel(100, bandwidth);
        spectrumAnalyzerHelper.SetRxSpectrumModel(sm);
        spectrumAnalyzerHelper.SetPhyAttribute("Resolution", TimeValue(MicroSeconds(10)));
        spectrumAnalyzerHelper.SetPhyAttribute("NoisePowerSpectralDensity",
                                               DoubleValue(1e-15)); // -120 dBm/Hz
        spectrumAnalyzerHelper.EnableAsciiAll("spectrum-analyzer-output");
        spectrumAnalyzerHelper.Install(spectrumAnalyzerNodes);
    }

    Ptr<RadioEnvironmentMapHelper> remHelper;
    if (generateRem)
    {
        PrintGnuplottableEnbListToFile("enbs.txt");
        PrintGnuplottableUeListToFile("ues.txt");

        remHelper = CreateObject<RadioEnvironmentMapHelper>();
        Ptr<LteSpectrumPhy> enbDlSpectrumPhy = enbDevs.Get(0)
                                                   ->GetObject<LteEnbNetDevice>()
                                                   ->GetPhy()
                                                   ->GetDownlinkSpectrumPhy()
                                                   ->GetObject<LteSpectrumPhy>();
        Ptr<SpectrumChannel> dlChannel = enbDlSpectrumPhy->GetChannel();
        uint32_t dlChannelId = dlChannel->GetId();
        NS_LOG_INFO("DL ChannelId: " << dlChannelId);
        remHelper->SetAttribute("Channel", PointerValue(dlChannel));
        remHelper->SetAttribute("OutputFile", StringValue("lena-distributed-ffr.rem"));
        remHelper->SetAttribute("XMin", DoubleValue(macroUeBox.xMin));
        remHelper->SetAttribute("XMax", DoubleValue(macroUeBox.xMax));
        remHelper->SetAttribute("YMin", DoubleValue(macroUeBox.yMin));
        remHelper->SetAttribute("YMax", DoubleValue(macroUeBox.yMax));
        remHelper->SetAttribute("Z", DoubleValue(1.5));
        remHelper->SetAttribute("XRes", UintegerValue(500));
        remHelper->SetAttribute("YRes", UintegerValue(500));

        if (remRbId >= 0)
        {
            remHelper->SetAttribute("UseDataChannel", BooleanValue(true));
            remHelper->SetAttribute("RbId", IntegerValue(remRbId));
        }

        remHelper->Install();
        // simulation will stop right after the REM has been generated
    }
    else
    {
        Simulator::Stop(Seconds(simTime));
    }

    Simulator::Run();
    Simulator::Destroy();
    return 0;
}
