/*
 * Copyright (c) 2022 Chandrakant Jena
 *
 * SPDX-License-Identifier: GPL-2.0-only
 *
 * Author: Chandrakant Jena <chandrakant.barcelona@gmail.com>
 */

/**
 * @file
 * Example to demonstrate the working of the Ping Application
 * Network topology:
 * @verbatim
   A ------------------------------ B ------------------------------ C
               100 Mbps                           100 Mbps
                 5 ms (one way)                     5 ms (one way)
   IPv4 addresses:
   10.1.1.1    <->         10.1.1.2 / 10.1.2.1     <->         10.1.2.2

   IPv6 addresses:
   2001:1::200:ff:fe00:1
       <->    2001:1::200:ff:fe00:2 / 2001:1:0:1:200:ff:fe00:3
                                          <-> 2001:1:0:1:200:ff:fe00:4
   @endverbatim
 *
 * The topology has three nodes interconnected by two point-to-point links.
 * Each link has 5 ms one-way delay, for a round-trip propagation delay
 * of 20 ms.  The transmission rate on each link is 100 Mbps.  The routing
 * between links is enabled by ns-3's NixVector routing.
 *
 * By default, this program will send 5 pings from node A to node C using IPv6.
 * When using IPv6, the output will look like this:
 * @verbatim
   PING 2001:1:0:1:200:ff:fe00:4 - 56 bytes of data; 104 bytes including ICMP and IPv6 headers.
   64 bytes from (2001:1:0:1:200:ff:fe00:4): icmp_seq=0 ttl=63 time=20.033 ms
   64 bytes from (2001:1:0:1:200:ff:fe00:4): icmp_seq=1 ttl=63 time=20.033 ms
   64 bytes from (2001:1:0:1:200:ff:fe00:4): icmp_seq=2 ttl=63 time=20.033 ms
   64 bytes from (2001:1:0:1:200:ff:fe00:4): icmp_seq=3 ttl=63 time=20.033 ms
   64 bytes from (2001:1:0:1:200:ff:fe00:4): icmp_seq=4 ttl=63 time=20.033 ms
   --- 2001:1:0:1:200:ff:fe00:4 ping statistics ---
   5 packets transmitted, 5 received, 0% packet loss, time 4020ms
   rtt min/avg/max/mdev = 20/20/20/0 ms
   @endverbatim
 *
 * When using IPv4, the output will look like this:
 * @verbatim
   PING 10.1.2.2 - 56 bytes of data; 84 bytes including ICMP and IPv4 headers.
   64 bytes from (10.1.2.2): icmp_seq=0 ttl=63 time=20.027 ms
   64 bytes from (10.1.2.2): icmp_seq=1 ttl=63 time=20.027 ms
   64 bytes from (10.1.2.2): icmp_seq=2 ttl=63 time=20.027 ms
   64 bytes from (10.1.2.2): icmp_seq=3 ttl=63 time=20.027 ms
   64 bytes from (10.1.2.2): icmp_seq=4 ttl=63 time=20.027 ms
   --- 10.1.2.2 ping statistics ---
   5 packets transmitted, 5 received, 0% packet loss, time 4020ms
   rtt min/avg/max/mdev = 20/20/20/0 ms
   @endverbatim
 *
 * The example program will also produce four pcap traces (one for each
 * NetDevice in the scenario) that can be viewed using tcpdump or Wireshark.
 *
 * Other program options include options to change the destination and
 * source addresses, number of packets (count), packet size, interval,
 * and whether to enable logging (if logging is enabled in the build).
 *
 * The Ping application in this example starts at simulation time 1 and will
 * stop either at simulation time 50 or once 'Count' pings have been responded
 * to, whichever comes first.
 */

#include "ns3/core-module.h"
#include "ns3/internet-apps-module.h"
#include "ns3/internet-module.h"
#include "ns3/network-module.h"
#include "ns3/nix-vector-routing-module.h"
#include "ns3/point-to-point-module.h"

#include <fstream>
#include <optional>

using namespace ns3;

NS_LOG_COMPONENT_DEFINE("PingExample");

int
main(int argc, char* argv[])
{
    bool logging{false};
    Time interPacketInterval{Seconds(1)};
    uint32_t size{56};
    uint32_t count{5};
    std::string destinationStr;
    Address destination;
    std::string sourceStr;
    Address source;
    bool useIpv6{true};
    std::optional<Ipv4Address> v4Dst;
    std::optional<Ipv6Address> v6Dst;
    std::optional<Ipv4Address> v4Src;
    std::optional<Ipv6Address> v6Src;

    GlobalValue::Bind("ChecksumEnabled", BooleanValue(true));

    CommandLine cmd(__FILE__);
    cmd.AddValue("logging", "Tell application to log if true", logging);
    cmd.AddValue("interval", "The time to wait between two packets", interPacketInterval);
    cmd.AddValue("size", "Data bytes to be sent, per-packet", size);
    cmd.AddValue("count", "Number of packets to be sent", count);
    cmd.AddValue("destination",
                 "Destination IPv4 or IPv6 address, e.g., \"10.1.2.2\"",
                 destinationStr);
    cmd.AddValue("source",
                 "Source address, needed only for multicast or broadcast destinations",
                 sourceStr);
    cmd.Parse(argc, argv);

    if (!destinationStr.empty())
    {
        if (Ipv4Address::CheckCompatible(destinationStr))
        {
            v4Dst = Ipv4Address(destinationStr.c_str());
            useIpv6 = false;
            destination = v4Dst.value();
        }
        else if (Ipv6Address::CheckCompatible(destinationStr))
        {
            v6Dst = Ipv6Address(destinationStr.c_str());
            useIpv6 = true;
            destination = v6Dst.value();
        }
        else
        {
            NS_ABORT_MSG("Can't parse destination address " << destinationStr);
        }
    }

    if (!sourceStr.empty())
    {
        if (Ipv4Address::CheckCompatible(sourceStr))
        {
            v4Src = Ipv4Address(sourceStr.c_str());
            useIpv6 = false;
            source = v4Src.value();
        }
        else if (Ipv6Address::CheckCompatible(sourceStr))
        {
            v6Src = Ipv6Address(sourceStr.c_str());
            useIpv6 = true;
            source = v6Src.value();
        }
        else
        {
            NS_ABORT_MSG("Can't parse destination address " << destinationStr);
        }
    }
    if (sourceStr.empty())
    {
        if (useIpv6)
        {
            if (v6Dst && v6Dst->IsMulticast())
            {
                std::cout << "Specify a source address to use when pinging multicast addresses"
                          << std::endl;
                std::cout << "Program exiting..." << std::endl;
                return 0;
            }
        }
        else
        {
            if (v4Dst && (v4Dst->IsBroadcast() || v4Dst->IsMulticast()))
            {
                std::cout << "Specify a source address to use when pinging broadcast or multicast "
                             "addresses"
                          << std::endl;
                std::cout << "Program exiting..." << std::endl;
                return 0;
            }
        }
    }

    if (logging)
    {
        LogComponentEnableAll(LogLevel(LOG_PREFIX_TIME | LOG_PREFIX_NODE));
        LogComponentEnable("Ping", LOG_LEVEL_ALL);
    }

    NodeContainer nodes;
    nodes.Create(3);
    NodeContainer link1Nodes;
    link1Nodes.Add(nodes.Get(0));
    link1Nodes.Add(nodes.Get(1));
    NodeContainer link2Nodes;
    link2Nodes.Add(nodes.Get(1));
    link2Nodes.Add(nodes.Get(2));

    PointToPointHelper pointToPoint;
    pointToPoint.SetDeviceAttribute("DataRate", StringValue("100Mbps"));
    pointToPoint.SetChannelAttribute("Delay", StringValue("5ms"));

    NetDeviceContainer link1Devices;
    link1Devices = pointToPoint.Install(link1Nodes);
    NetDeviceContainer link2Devices;
    link2Devices = pointToPoint.Install(link2Nodes);

    // The following block of code inserts an optional packet loss model
    Ptr<PointToPointNetDevice> p2pSender = DynamicCast<PointToPointNetDevice>(link1Devices.Get(0));
    Ptr<ReceiveListErrorModel> errorModel = CreateObject<ReceiveListErrorModel>();
    std::list<uint32_t> dropList;
    // Enable one or more of the below lines to force specific packet losses
    // dropList.push_back (0);
    // dropList.push_back (1);
    // dropList.push_back (2);
    // dropList.push_back (3);
    // dropList.push_back (4);
    // etc. (other lines may be added)
    errorModel->SetList(dropList);
    p2pSender->SetReceiveErrorModel(errorModel);

    if (!useIpv6)
    {
        Ipv4NixVectorHelper nixRouting;
        InternetStackHelper stack;
        stack.SetRoutingHelper(nixRouting);
        stack.SetIpv6StackInstall(false);
        stack.Install(nodes);

        Ipv4AddressHelper addressV4;
        addressV4.SetBase("10.1.1.0", "255.255.255.0");
        addressV4.Assign(link1Devices);
        addressV4.NewNetwork();
        Ipv4InterfaceContainer link2InterfacesV4 = addressV4.Assign(link2Devices);

        if (destination.IsInvalid())
        {
            destination = link2InterfacesV4.GetAddress(1, 0);
        }
    }
    else
    {
        Ipv6NixVectorHelper nixRouting;
        InternetStackHelper stack;
        stack.SetRoutingHelper(nixRouting);
        stack.SetIpv4StackInstall(false);
        stack.Install(nodes);

        Ipv6AddressHelper addressV6;
        addressV6.SetBase("2001:1::", 64);
        addressV6.Assign(link1Devices);
        addressV6.NewNetwork();
        Ipv6InterfaceContainer link2InterfacesV6 = addressV6.Assign(link2Devices);

        if (destination.IsInvalid())
        {
            destination = link2InterfacesV6.GetAddress(1, 1);
        }
    }

    // Create Ping application and installing on node A
    PingHelper pingHelper(destination, source);
    pingHelper.SetAttribute("Interval", TimeValue(interPacketInterval));
    pingHelper.SetAttribute("Size", UintegerValue(size));
    pingHelper.SetAttribute("Count", UintegerValue(count));
    ApplicationContainer apps = pingHelper.Install(nodes.Get(0));
    apps.Start(Seconds(1));
    apps.Stop(Seconds(50));

    pointToPoint.EnablePcapAll("ping-example");

    Simulator::Stop(Seconds(60));
    Simulator::Run();
    Simulator::Destroy();
    return 0;
}
