/*
 * Copyright (c) 2011 INRIA
 *
 * SPDX-License-Identifier: GPL-2.0-only
 *
 * Author: Claudio Freire <claudio-daniel.freire@inria.fr>
 */
#include "ns3/calendar-scheduler.h"
#include "ns3/config.h"
#include "ns3/heap-scheduler.h"
#include "ns3/list-scheduler.h"
#include "ns3/map-scheduler.h"
#include "ns3/simulator.h"
#include "ns3/string.h"
#include "ns3/test.h"

#include <chrono> // seconds, milliseconds
#include <list>
#include <thread> // sleep_for
#include <utility>

using namespace ns3;

/// Maximum number of threads.
constexpr int MAXTHREADS = 64;

/**
 * @file
 * @ingroup threaded-tests
 * Threaded events test suite
 */

/**
 * @ingroup core-tests
 * @defgroup threaded-tests Threaded events tests
 */

/**
 * @ingroup threaded-tests
 *
 * @brief Check threaded event handling with various thread number, schedulers, and  simulator
 * types.
 */
class ThreadedSimulatorEventsTestCase : public TestCase
{
  public:
    /**
     * Constructor.
     *
     * @param schedulerFactory The scheduler factory.
     * @param simulatorType The simulator type.
     * @param threads The number of threads.
     */
    ThreadedSimulatorEventsTestCase(ObjectFactory schedulerFactory,
                                    const std::string& simulatorType,
                                    unsigned int threads);
    /**
     * Event A
     * @param a The Event parameter.
     */
    void EventA(int a);
    /**
     * Event B
     * @param b The Event parameter.
     */
    void EventB(int b);
    /**
     * Event C
     * @param c The Event parameter.
     */
    void EventC(int c);
    /**
     * Event D
     * @param d The Event parameter.
     */
    void EventD(int d);
    /**
     * No-op function, records the thread that called it.
     * @param threadno The thread number.
     */
    void DoNothing(unsigned int threadno);
    /**
     * Schedule a thread.
     * @param context The context.
     */
    static void SchedulingThread(std::pair<ThreadedSimulatorEventsTestCase*, unsigned int> context);
    /**
     * End the thread execution.
     */
    void End();
    uint64_t m_a;                        //!< The value incremented when EventA is called.
    uint64_t m_b;                        //!< The value incremented when EventB is called.
    uint64_t m_c;                        //!< The value incremented when EventC is called.
    uint64_t m_d;                        //!< The value incremented when EventD is called.
    unsigned int m_threads;              //!< The number of threads.
    bool m_threadWaiting[MAXTHREADS];    //!< Threads waiting to be scheduled.
    bool m_stop;                         //!< Stop variable.
    ObjectFactory m_schedulerFactory;    //!< Scheduler factory.
    std::string m_simulatorType;         //!< Simulator type.
    std::string m_error;                 //!< Error condition.
    std::list<std::thread> m_threadlist; //!< Thread list.

  private:
    void DoSetup() override;
    void DoRun() override;
    void DoTeardown() override;
};

ThreadedSimulatorEventsTestCase::ThreadedSimulatorEventsTestCase(ObjectFactory schedulerFactory,
                                                                 const std::string& simulatorType,
                                                                 unsigned int threads)
    : TestCase("Check threaded event handling with " + std::to_string(threads) + " threads, " +
               schedulerFactory.GetTypeId().GetName() + " scheduler, in " + simulatorType),
      m_threads(threads),
      m_schedulerFactory(schedulerFactory),
      m_simulatorType(simulatorType)
{
}

void
ThreadedSimulatorEventsTestCase::End()
{
    m_stop = true;
    for (auto& thread : m_threadlist)
    {
        if (thread.joinable())
        {
            thread.join();
        }
    }
}

void
ThreadedSimulatorEventsTestCase::SchedulingThread(
    std::pair<ThreadedSimulatorEventsTestCase*, unsigned int> context)
{
    ThreadedSimulatorEventsTestCase* me = context.first;
    unsigned int threadno = context.second;

    while (!me->m_stop)
    {
        me->m_threadWaiting[threadno] = true;
        Simulator::ScheduleWithContext(threadno,
                                       MicroSeconds(1),
                                       &ThreadedSimulatorEventsTestCase::DoNothing,
                                       me,
                                       threadno);
        while (!me->m_stop && me->m_threadWaiting[threadno])
        {
            std::this_thread::sleep_for(std::chrono::nanoseconds(500));
        }
    }
}

void
ThreadedSimulatorEventsTestCase::DoNothing(unsigned int threadno)
{
    if (!m_error.empty())
    {
        m_error = "Bad threaded scheduling";
    }
    m_threadWaiting[threadno] = false;
}

void
ThreadedSimulatorEventsTestCase::EventA(int a)
{
    if (m_a != m_b || m_a != m_c || m_a != m_d)
    {
        m_error = "Bad scheduling";
        Simulator::Stop();
    }
    ++m_a;
    Simulator::Schedule(MicroSeconds(10), &ThreadedSimulatorEventsTestCase::EventB, this, a + 1);
}

void
ThreadedSimulatorEventsTestCase::EventB(int b)
{
    if (m_a != (m_b + 1) || m_a != (m_c + 1) || m_a != (m_d + 1))
    {
        m_error = "Bad scheduling";
        Simulator::Stop();
    }
    ++m_b;
    Simulator::Schedule(MicroSeconds(10), &ThreadedSimulatorEventsTestCase::EventC, this, b + 1);
}

void
ThreadedSimulatorEventsTestCase::EventC(int c)
{
    if (m_a != m_b || m_a != (m_c + 1) || m_a != (m_d + 1))
    {
        m_error = "Bad scheduling";
        Simulator::Stop();
    }
    ++m_c;
    Simulator::Schedule(MicroSeconds(10), &ThreadedSimulatorEventsTestCase::EventD, this, c + 1);
}

void
ThreadedSimulatorEventsTestCase::EventD(int d)
{
    if (m_a != m_b || m_a != m_c || m_a != (m_d + 1))
    {
        m_error = "Bad scheduling";
        Simulator::Stop();
    }
    ++m_d;
    if (m_stop)
    {
        Simulator::Stop();
    }
    else
    {
        Simulator::Schedule(MicroSeconds(10),
                            &ThreadedSimulatorEventsTestCase::EventA,
                            this,
                            d + 1);
    }
}

void
ThreadedSimulatorEventsTestCase::DoSetup()
{
    if (!m_simulatorType.empty())
    {
        Config::SetGlobal("SimulatorImplementationType", StringValue(m_simulatorType));
    }

    m_error = "";

    m_a = m_b = m_c = m_d = 0;
}

void
ThreadedSimulatorEventsTestCase::DoTeardown()
{
    m_threadlist.clear();

    Config::SetGlobal("SimulatorImplementationType", StringValue("ns3::DefaultSimulatorImpl"));
}

void
ThreadedSimulatorEventsTestCase::DoRun()
{
    m_stop = false;
    Simulator::SetScheduler(m_schedulerFactory);

    Simulator::Schedule(MicroSeconds(10), &ThreadedSimulatorEventsTestCase::EventA, this, 1);
    Simulator::Schedule(Seconds(1), &ThreadedSimulatorEventsTestCase::End, this);

    for (unsigned int i = 0; i < m_threads; ++i)
    {
        m_threadlist.emplace_back(
            &ThreadedSimulatorEventsTestCase::SchedulingThread,
            std::pair<ThreadedSimulatorEventsTestCase*, unsigned int>(this, i));
    }

    Simulator::Run();
    Simulator::Destroy();

    NS_TEST_EXPECT_MSG_EQ(m_error.empty(), true, m_error);
    NS_TEST_EXPECT_MSG_EQ(m_a, m_b, "Bad scheduling");
    NS_TEST_EXPECT_MSG_EQ(m_a, m_c, "Bad scheduling");
    NS_TEST_EXPECT_MSG_EQ(m_a, m_d, "Bad scheduling");
}

/**
 * @ingroup threaded-tests
 *
 * @brief The threaded simulator Test Suite.
 */
class ThreadedSimulatorTestSuite : public TestSuite
{
  public:
    ThreadedSimulatorTestSuite()
        : TestSuite("threaded-simulator")
    {
        std::string simulatorTypes[] = {
            "ns3::RealtimeSimulatorImpl",
            "ns3::DefaultSimulatorImpl",
        };
        std::string schedulerTypes[] = {
            "ns3::ListScheduler",
            "ns3::HeapScheduler",
            "ns3::MapScheduler",
            "ns3::CalendarScheduler",
        };
        unsigned int threadCounts[] = {0, 2, 10, 20};
        ObjectFactory factory;

        for (auto& simulatorType : simulatorTypes)
        {
            for (auto& schedulerType : schedulerTypes)
            {
                for (auto& threadCount : threadCounts)
                {
                    factory.SetTypeId(schedulerType);
                    AddTestCase(
                        new ThreadedSimulatorEventsTestCase(factory, simulatorType, threadCount),
                        TestCase::Duration::QUICK);
                }
            }
        }
    }
};

/// Static variable for test initialization.
static ThreadedSimulatorTestSuite g_threadedSimulatorTestSuite;
