/*
 * Copyright (c) 2013 University of Washington
 *
 * SPDX-License-Identifier: GPL-2.0-only
 *
 * Author: Mitch Watrous (watrous@u.washington.edu)
 */

#include "gnuplot-helper.h"

#include "ns3/abort.h"
#include "ns3/assert.h"
#include "ns3/config.h"
#include "ns3/get-wildcard-matches.h"
#include "ns3/log.h"

#include <fstream>
#include <iostream>
#include <sstream>
#include <string>

namespace ns3
{

NS_LOG_COMPONENT_DEFINE("GnuplotHelper");

GnuplotHelper::GnuplotHelper()
    : m_aggregator(nullptr),
      m_plotProbeCount(0),
      m_outputFileNameWithoutExtension("gnuplot-helper"),
      m_title("Gnuplot Helper Plot"),
      m_xLegend("X Values"),
      m_yLegend("Y Values"),
      m_terminalType("png")
{
    NS_LOG_FUNCTION(this);

    // Note that this does not construct an aggregator. It will be
    // constructed later when needed.
}

GnuplotHelper::GnuplotHelper(const std::string& outputFileNameWithoutExtension,
                             const std::string& title,
                             const std::string& xLegend,
                             const std::string& yLegend,
                             const std::string& terminalType)
    : m_aggregator(nullptr),
      m_plotProbeCount(0),
      m_outputFileNameWithoutExtension(outputFileNameWithoutExtension),
      m_title(title),
      m_xLegend(xLegend),
      m_yLegend(yLegend),
      m_terminalType(terminalType)
{
    NS_LOG_FUNCTION(this);

    // Construct the aggregator.
    ConstructAggregator();
}

GnuplotHelper::~GnuplotHelper()
{
    NS_LOG_FUNCTION(this);
}

void
GnuplotHelper::ConfigurePlot(const std::string& outputFileNameWithoutExtension,
                             const std::string& title,
                             const std::string& xLegend,
                             const std::string& yLegend,
                             const std::string& terminalType)
{
    NS_LOG_FUNCTION(this << outputFileNameWithoutExtension << title << xLegend << yLegend
                         << terminalType);

    // See if an aggregator has already been constructed.
    if (m_aggregator)
    {
        NS_LOG_WARN("An existing aggregator object "
                    << m_aggregator << " may be destroyed if no references remain.");
    }

    // Store these so that they can be used to construct the aggregator.
    m_outputFileNameWithoutExtension = outputFileNameWithoutExtension;
    m_title = title;
    m_xLegend = xLegend;
    m_yLegend = yLegend;
    m_terminalType = terminalType;

    // Construct the aggregator.
    ConstructAggregator();
}

void
GnuplotHelper::PlotProbe(const std::string& typeId,
                         const std::string& path,
                         const std::string& probeTraceSource,
                         const std::string& title,
                         GnuplotAggregator::KeyLocation keyLocation)
{
    NS_LOG_FUNCTION(this << typeId << path << probeTraceSource << title << keyLocation);

    // Get a pointer to the aggregator.
    Ptr<GnuplotAggregator> aggregator = GetAggregator();

    // Add a subtitle to the title to show the trace source's path.
    aggregator->SetTitle(m_title + " \\n\\nTrace Source Path: " + path);

    // Set the default dataset plotting style for the values.
    GnuplotAggregator::Set2dDatasetDefaultStyle(Gnuplot2dDataset::LINES_POINTS);

    // Set the location of the key in the plot.
    aggregator->SetKeyLocation(keyLocation);

    std::string pathWithoutLastToken;
    std::string lastToken;

    // See if the path has any wildcards.
    bool pathHasNoWildcards = path.find('*') == std::string::npos;

    // Remove the last token from the path; this should correspond to the
    // trace source attribute.
    size_t lastSlash = path.find_last_of('/');
    if (lastSlash == std::string::npos)
    {
        pathWithoutLastToken = path;
        lastToken = "";
    }
    else
    {
        // Chop off up to last token.
        pathWithoutLastToken = path.substr(0, lastSlash);

        // Save the last token without the last slash.
        lastToken = path.substr(lastSlash + 1, std::string::npos);
    }

    // See if there are any matches for the probe's path with the last
    // token removed; this corresponds to the traced object itself.
    NS_LOG_DEBUG("Searching config database for trace source " << path);
    Config::MatchContainer matches = Config::LookupMatches(pathWithoutLastToken);
    uint32_t matchCount = matches.GetN();
    NS_LOG_DEBUG("Found " << matchCount << " matches for trace source " << path);

    // This is used to make the probe's context be unique.
    std::string matchIdentifier;

    // Hook one or more probes and the aggregator together.
    if (matchCount == 1 && pathHasNoWildcards)
    {
        // Connect the probe to the aggregator only once because there
        // is only one matching config path.  There is no need to find
        // the wildcard matches because the passed in path has none.
        matchIdentifier = "0";
        ConnectProbeToAggregator(typeId, matchIdentifier, path, probeTraceSource, title);
    }
    else if (matchCount > 0)
    {
        // Handle all of the matches if there are more than one.
        for (uint32_t i = 0; i < matchCount; i++)
        {
            // Set the match identifier.
            std::ostringstream matchIdentifierStream;
            matchIdentifierStream << i;
            matchIdentifier = matchIdentifierStream.str();

            // Construct the matched path and get the matches for each
            // of the wildcards.
            std::string wildcardSeparator = " ";
            std::string matchedPath = matches.GetMatchedPath(i) + lastToken;
            std::string wildcardMatches = GetWildcardMatches(path, matchedPath, wildcardSeparator);

            // Connect the probe to the aggregator for this match.
            ConnectProbeToAggregator(typeId,
                                     matchIdentifier,
                                     matchedPath,
                                     probeTraceSource,
                                     title + "-" + wildcardMatches);
        }
    }
    else
    {
        // There is a problem if there are no matching config paths.
        NS_FATAL_ERROR("Lookup of " << path << " got no matches");
    }
}

void
GnuplotHelper::AddProbe(const std::string& typeId,
                        const std::string& probeName,
                        const std::string& path)
{
    NS_LOG_FUNCTION(this << typeId << probeName << path);

    // See if this probe had already been added.
    if (m_probeMap.count(probeName) > 0)
    {
        NS_ABORT_MSG("That probe has already been added");
    }

    // Prepare the factory to create an object with the requested type.
    m_factory.SetTypeId(typeId);

    // Create a base class object in order to validate the type.
    Ptr<Probe> probe = m_factory.Create()->GetObject<Probe>();
    if (!probe)
    {
        NS_ABORT_MSG("The requested type is not a probe");
    }

    // Set the probe's name.
    probe->SetName(probeName);

    // Set the path.  Note that no return value is checked here.
    probe->ConnectByPath(path);

    // Enable logging of data for the probe.
    probe->Enable();

    // Add this probe to the map so that its values can be used.
    m_probeMap[probeName] = std::make_pair(probe, typeId);
}

void
GnuplotHelper::AddTimeSeriesAdaptor(const std::string& adaptorName)
{
    NS_LOG_FUNCTION(this << adaptorName);

    // See if this time series adaptor had already been added.
    if (m_timeSeriesAdaptorMap.count(adaptorName) > 0)
    {
        NS_ABORT_MSG("That time series adaptor has already been added");
    }

    // Create the time series adaptor.
    Ptr<TimeSeriesAdaptor> timeSeriesAdaptor = CreateObject<TimeSeriesAdaptor>();

    // Enable logging of data for the time series adaptor.
    timeSeriesAdaptor->Enable();

    // Add this time series adaptor to the map so that can be used.
    m_timeSeriesAdaptorMap[adaptorName] = timeSeriesAdaptor;
}

Ptr<Probe>
GnuplotHelper::GetProbe(std::string probeName) const
{
    // Look for the probe.
    auto mapIterator = m_probeMap.find(probeName);

    // Return the probe if it has been added.
    if (mapIterator != m_probeMap.end())
    {
        return mapIterator->second.first;
    }
    else
    {
        NS_ABORT_MSG("That probe has not been added");
    }
}

Ptr<GnuplotAggregator>
GnuplotHelper::GetAggregator()
{
    NS_LOG_FUNCTION(this);

    // Do a lazy construction of the aggregator if it hasn't already
    // been constructed.
    if (!m_aggregator)
    {
        ConstructAggregator();
    }
    return m_aggregator;
}

void
GnuplotHelper::ConstructAggregator()
{
    NS_LOG_FUNCTION(this);

    // Create the aggregator.
    m_aggregator = CreateObject<GnuplotAggregator>(m_outputFileNameWithoutExtension);

    // Set the aggregator's properties.
    m_aggregator->SetTerminal(m_terminalType);
    m_aggregator->SetTitle(m_title);
    m_aggregator->SetLegend(m_xLegend, m_yLegend);

    // Enable logging of data for the aggregator.
    m_aggregator->Enable();
}

void
GnuplotHelper::ConnectProbeToAggregator(const std::string& typeId,
                                        const std::string& matchIdentifier,
                                        const std::string& path,
                                        const std::string& probeTraceSource,
                                        const std::string& title)
{
    NS_LOG_FUNCTION(this << typeId << matchIdentifier << path << probeTraceSource << title);

    Ptr<GnuplotAggregator> aggregator = GetAggregator();

    // Increment the total number of plot probes that have been created.
    m_plotProbeCount++;

    // Create a unique name for this probe.
    std::ostringstream probeNameStream;
    probeNameStream << "PlotProbe-" << m_plotProbeCount;
    std::string probeName = probeNameStream.str();

    // Create a unique dataset context string for this probe.
    std::string probeContext = probeName + "/" + matchIdentifier + "/" + probeTraceSource;

    // Add the probe to the map of probes, which will keep the probe in
    // memory after this function ends.
    AddProbe(typeId, probeName, path);

    // Because the callbacks to the probes' trace sources don't use the
    // probe's context, a unique adaptor needs to be created for each
    // probe context so that information is not lost.
    AddTimeSeriesAdaptor(probeContext);

    // Connect the probe to the adaptor.
    if (m_probeMap[probeName].second == "ns3::DoubleProbe" ||
        m_probeMap[probeName].second == "ns3::TimeProbe")
    {
        m_probeMap[probeName].first->TraceConnectWithoutContext(
            probeTraceSource,
            MakeCallback(&TimeSeriesAdaptor::TraceSinkDouble,
                         m_timeSeriesAdaptorMap[probeContext]));
    }
    else if (m_probeMap[probeName].second == "ns3::BooleanProbe")
    {
        m_probeMap[probeName].first->TraceConnectWithoutContext(
            probeTraceSource,
            MakeCallback(&TimeSeriesAdaptor::TraceSinkBoolean,
                         m_timeSeriesAdaptorMap[probeContext]));
    }
    else if (m_probeMap[probeName].second == "ns3::Uinteger32Probe" ||
             m_probeMap[probeName].second == "ns3::PacketProbe" ||
             m_probeMap[probeName].second == "ns3::ApplicationPacketProbe" ||
             m_probeMap[probeName].second == "ns3::Ipv4PacketProbe" ||
             m_probeMap[probeName].second == "ns3::Ipv6PacketProbe")
    {
        m_probeMap[probeName].first->TraceConnectWithoutContext(
            probeTraceSource,
            MakeCallback(&TimeSeriesAdaptor::TraceSinkUinteger32,
                         m_timeSeriesAdaptorMap[probeContext]));
    }
    else if (m_probeMap[probeName].second == "ns3::Uinteger8Probe")
    {
        m_probeMap[probeName].first->TraceConnectWithoutContext(
            probeTraceSource,
            MakeCallback(&TimeSeriesAdaptor::TraceSinkUinteger8,
                         m_timeSeriesAdaptorMap[probeContext]));
    }
    else if (m_probeMap[probeName].second == "ns3::Uinteger16Probe")
    {
        m_probeMap[probeName].first->TraceConnectWithoutContext(
            probeTraceSource,
            MakeCallback(&TimeSeriesAdaptor::TraceSinkUinteger16,
                         m_timeSeriesAdaptorMap[probeContext]));
    }
    else
    {
        NS_FATAL_ERROR("Unknown probe type " << m_probeMap[probeName].second
                                             << "; need to add support in the helper for this");
    }

    // Connect the adaptor to the aggregator.
    std::string adaptorTraceSource = "Output";
    m_timeSeriesAdaptorMap[probeContext]->TraceConnect(
        adaptorTraceSource,
        probeContext,
        MakeCallback(&GnuplotAggregator::Write2d, aggregator));

    // Add the dataset to the plot.
    aggregator->Add2dDataset(probeContext, title);
}

} // namespace ns3
