// =====================================================================
//  THREAD LAB — Basics of Thread Programming in C#
// =====================================================================
//
//  Setup:
//    dotnet new console -n ThreadLab
//    -> replace the generated Program.cs with this file
//
//  Run (use Release mode for meaningful timings!):
//    dotnet run -c Release
//
//  Work through the parts IN ORDER. Each part can be run from the menu,
//  so test your solution after every TODO before moving on.
//
//    Part 1  Download manager   – creating and starting threads
//    Part 2  Download manager   – joining threads
//    Part 3  Prime hunter       – CPU speed-up with threads
//    Part 4  Shared counter     – race conditions and lock
//    Part 5  Starting gun       – semaphore as a start signal
//    Part 6  Ping-pong          – semaphores for turn-taking
//
//  Every TODO is numbered (TODO 1.1, 1.2, ...). The exercise sheet
//  contains the matching questions you should answer.
// =====================================================================
 
using System.Diagnostics;
 
namespace ThreadLab;
 
public static class Program
{
    // Short id of the thread currently executing — handy for log output.
    private static string Tid => $"T{Environment.CurrentManagedThreadId,-2}";
 
    public static void Main()
    {
        Console.WriteLine("=== Thread Lab ===");
        Console.WriteLine($"This machine has {Environment.ProcessorCount} logical processors.");
 
        while (true)
        {
            Console.WriteLine();
            Console.WriteLine("Choose a part:");
            Console.WriteLine("  1 - Download manager: starting threads");
            Console.WriteLine("  2 - Download manager: joining threads");
            Console.WriteLine("  3 - Prime hunter: CPU speed-up");
            Console.WriteLine("  4 - Shared counter: lock");
            Console.WriteLine("  5 - Starting gun: semaphore as a signal");
            Console.WriteLine("  6 - Ping-pong: semaphores for turn-taking");
            Console.WriteLine("  0 - Exit");
            Console.Write("> ");
 
            switch (Console.ReadLine()?.Trim())
            {
                case "1": Part1_StartingThreads(); break;
                case "2": Part2_JoiningThreads(); break;
                case "3": Part3_PrimeHunter(); break;
                case "4": Part4_SharedCounter(); break;
                case "5": Part5_StartingGun(); break;
                case "6": Part6_PingPong(); break;
                case "0": return;
                default: Console.WriteLine("Unknown option."); break;
            }
        }
    }
 
    private static void Header(string title)
    {
        Console.WriteLine();
        Console.WriteLine(new string('=', 60));
        Console.WriteLine($" {title}");
        Console.WriteLine(new string('=', 60));
    }
 
    // =================================================================
    //  PART 1 + 2 — DOWNLOAD MANAGER (independent parallel workloads)
    // =================================================================
    //  A "download" is simulated with Thread.Sleep. The CPU does almost
    //  nothing while waiting — just like a real download waits for the
    //  network. Threads let us wait for many things at the same time.
 
    private static readonly (string Name, int DurationMs)[] Downloads =
    {
        ("cat-video.mp4",     2000),
        ("lecture-notes.pdf",  800),
        ("game-patch.zip",    1600),
        ("playlist.mp3",      1200),
        ("dataset.csv",       1000),
    };
 
    // Given — do not change. Simulates downloading a file in 4 steps.
    private static void SimulateDownload(string fileName, int durationMs)
    {
        const int steps = 4;
        for (int i = 1; i <= steps; i++)
        {
            Thread.Sleep(durationMs / steps);
            Console.WriteLine($"  [{Tid}] {fileName,-18} {i * 100 / steps,3}%");
        }
    }
 
    private static void Part1_StartingThreads()
    {
        Header("Part 1 — Starting threads");
 
        // ---- 1.1 Sequential version (given) -------------------------
        // Run it first. Note the thread id and the total time.
        Console.WriteLine("Sequential download:");
        var sw = Stopwatch.StartNew();
        foreach (var (name, ms) in Downloads)
        {
            SimulateDownload(name, ms);
        }
        sw.Stop();
        Console.WriteLine($"Sequential: all downloads finished after {sw.ElapsedMilliseconds} ms");
 
        Console.WriteLine();
        Console.WriteLine("Press Enter to start the parallel version...");
        Console.ReadLine();
 
        // ---- 1.2 Parallel version ------------------------------------
        sw.Restart();
 
        // TODO 1.2: For EVERY entry in Downloads, create a new Thread that
        //           calls SimulateDownload(name, ms) and start it.
        //           Hint: new Thread(() => ...) with a lambda.
 
 
        // TODO 1.3: Run the program. Look at the time printed below and at
        //           the order of the output lines. Answer the questions on
        //           the exercise sheet. (Do NOT fix anything here — that's
        //           what Part 2 is for.)
        Console.WriteLine($"Parallel: main thread reached this line after {sw.ElapsedMilliseconds} ms");
    }
 
    private static void Part2_JoiningThreads()
    {
        Header("Part 2 — Joining threads");
 
        int sequentialMs = Downloads.Sum(d => d.DurationMs);
        var sw = Stopwatch.StartNew();
 
        // TODO 2.1: Start one thread per download again (like in TODO 1.2),
        //           but this time remember every thread in a List<Thread>.
 
 
        // TODO 2.2: Wait until ALL threads have finished (Join).
        //           Only then may the main thread continue and stop the timer.
 
 
        sw.Stop();
        Console.WriteLine($"Parallel: all downloads finished after {sw.ElapsedMilliseconds} ms");
        Console.WriteLine($"Sequential would take about {sequentialMs} ms " +
                          $"-> speed-up: {(double)sequentialMs / Math.Max(1, sw.ElapsedMilliseconds):F2}x");
 
        // TODO 2.3: Experiment: call Join() directly after Start() inside
        //           your loop (instead of after the loop). Run it, compare
        //           the time, explain the result — then change it back.
    }
 
    // =================================================================
    //  PART 3 — PRIME HUNTER (CPU-bound speed-up)
    // =================================================================
    //  Counting primes keeps the CPU busy. One thread uses one core —
    //  several threads can use several cores at the same time.
 
    private const int PrimeLimit = 5_000_000; // increase if it runs too fast
 
    // Given — do not change.
    private static bool IsPrime(int n)
    {
        if (n < 2) return false;
        if (n % 2 == 0) return n == 2;
        for (int d = 3; (long)d * d <= n; d += 2)
        {
            if (n % d == 0) return false;
        }
        return true;
    }
 
    // Given — counts primes in [fromInclusive, toExclusive).
    private static int CountPrimes(int fromInclusive, int toExclusive)
    {
        int count = 0;
        for (int n = fromInclusive; n < toExclusive; n++)
        {
            if (IsPrime(n)) count++;
        }
        return count;
    }
 
    private static void Part3_PrimeHunter()
    {
        Header($"Part 3 — Prime hunter: counting primes below {PrimeLimit:N0}");
 
        var sw = Stopwatch.StartNew();
        int expected = CountPrimes(0, PrimeLimit);
        sw.Stop();
        long sequentialMs = sw.ElapsedMilliseconds;
        Console.WriteLine($"Sequential: {expected:N0} primes in {sequentialMs} ms");
        Console.WriteLine();
 
        int[] threadCounts = new[] { 1, 2, 4, Environment.ProcessorCount, Environment.ProcessorCount * 2 }
            .Distinct()
            .OrderBy(x => x)
            .ToArray();
 
        Console.WriteLine("Threads |     Time | Speed-up | Result");
        Console.WriteLine("--------+----------+----------+------------------");
        foreach (int threadCount in threadCounts)
        {
            sw.Restart();
            int result = CountPrimesParallel(PrimeLimit, threadCount);
            sw.Stop();
 
            if (result < 0)
            {
                Console.WriteLine("CountPrimesParallel is not implemented yet (TODO 3.1).");
                return;
            }
 
            string check = result == expected ? "OK" : "WRONG!";
            double speedUp = (double)sequentialMs / Math.Max(1, sw.ElapsedMilliseconds);
            Console.WriteLine($"{threadCount,7} | {sw.ElapsedMilliseconds,5} ms | {speedUp,7:F2}x | {result:N0} {check}");
        }
 
        // TODO 3.2: Run Part 3 a few times and answer the questions on the
        //           exercise sheet (best thread count, why not more?).
    }
 
    private static int CountPrimesParallel(int limit, int threadCount)
    {
        // TODO 3.1: Split the range [0, limit) into threadCount chunks of
        //           (roughly) equal size. Start one thread per chunk that
        //           calls CountPrimes(from, to) for its chunk.
        //
        //           Every thread stores its partial result in its OWN slot
        //           of an array:   int[] results = new int[threadCount];
        //           -> no two threads write to the same memory location,
        //              so no synchronisation is needed (yet).
        //
        //           Join all threads, then return the sum of all results.
        //
        //           Watch out:
        //           - The last chunk must reach exactly up to 'limit'
        //             (limit is not always divisible by threadCount).
        //           - A lambda captures VARIABLES, not values. Copy the loop
        //             variable into a local (int index = i;) before using it
        //             inside the lambda.
 
        return -1; // TODO 3.1: remove this line when you are done
    }
 
    // TODO 3.3 (bonus): Thread 0 gets the small numbers, the last thread the
    //           big numbers — which are much more expensive to test. Write a
    //           second version where thread k checks the numbers
    //           k, k + threadCount, k + 2*threadCount, ...
    //           Compare the speed-ups of both versions.
 
    // =================================================================
    //  PART 4 — SHARED COUNTER (race conditions and lock)
    // =================================================================
 
    private const int CounterThreads = 8;
    private const int IncrementsPerThread = 1_000_000;
 
    private static int _sharedCounter = 0;
    private static readonly object _counterLock = new();
 
    private static void Part4_SharedCounter()
    {
        Header($"Part 4 — Shared counter ({CounterThreads} threads x {IncrementsPerThread:N0} increments)");
 
        RunCounterExperiment("4.1 no synchronisation", IncrementUnsafe);
        RunCounterExperiment("4.2 lock per increment", IncrementWithLock);
        RunCounterExperiment("4.3 count locally, lock once", IncrementLocalThenLock);
    }
 
    // Given — runs 'work' on CounterThreads threads and checks the result.
    private static void RunCounterExperiment(string label, Action work)
    {
        _sharedCounter = 0;
        var threads = new List<Thread>();
 
        var sw = Stopwatch.StartNew();
        for (int i = 0; i < CounterThreads; i++)
        {
            var t = new Thread(() => work());
            threads.Add(t);
            t.Start();
        }
        foreach (var t in threads)
        {
            t.Join();
        }
        sw.Stop();
 
        int expected = CounterThreads * IncrementsPerThread;
        string verdict = _sharedCounter == expected
            ? "correct"
            : $"WRONG, lost {expected - _sharedCounter:N0}";
        Console.WriteLine($"{label,-30} {_sharedCounter,11:N0} / {expected:N0}  {sw.ElapsedMilliseconds,5} ms  ({verdict})");
    }
 
    // 4.1 Given — run Part 4 several times and look at the first line.
    private static void IncrementUnsafe()
    {
        for (int i = 0; i < IncrementsPerThread; i++)
        {
            _sharedCounter++;
        }
    }
 
    private static void IncrementWithLock()
    {
        // TODO 4.2: Same loop as in IncrementUnsafe, but protect every
        //           single increment with lock (_counterLock) { ... }.
    }
 
    private static void IncrementLocalThenLock()
    {
        // TODO 4.3: Count in a LOCAL variable first (local variables are
        //           private to each thread!). When the loop is done, add
        //           the local count to _sharedCounter — inside ONE lock.
    }
 
    // TODO 4.4 (bonus): Go back to CountPrimesParallel (Part 3) and replace
    //           the results array by one shared total that every thread
    //           adds its partial result to. Which lock strategy from
    //           4.2 / 4.3 do you use — and why?
 
    // =================================================================
    //  PART 5 — STARTING GUN (semaphore as a start signal)
    // =================================================================
 
    private static readonly string[] Runners = { "Ada", "Linus", "Grace", "Bjarne", "Margaret" };
 
    private static SemaphoreSlim _startSignal = new(0);
    private static readonly List<string> _finishOrder = new();
    private static readonly object _finishLock = new();
 
    private static void Part5_StartingGun()
    {
        Header("Part 5 — Starting gun");
 
        _startSignal = new SemaphoreSlim(0); // 0 permits -> Wait() blocks
        _finishOrder.Clear();
 
        // TODO 5.1: Start one thread per runner, each executing Runner(name).
        //           Keep the threads in a list (you need them in TODO 5.5).
        //           Run it now: what happens before the countdown is over?
 
 
        Thread.Sleep(500); // give the runners time to reach the starting line
        foreach (var s in new[] { "3...", "2...", "1..." })
        {
            Console.WriteLine($"  [{Tid}] {s}");
            Thread.Sleep(1000);
        }
        Console.WriteLine($"  [{Tid}] GO!");
 
        // TODO 5.3: Fire the starting gun: release the semaphore so that ALL
        //           runners may start. How many permits do you need?
 
 
        // TODO 5.5: Wait for all runners to finish, then print the ranking
        //           stored in _finishOrder (1. ..., 2. ..., ...).
    }
 
    private static void Runner(string name)
    {
        Console.WriteLine($"  [{Tid}] {name} is at the starting line.");
 
        // TODO 5.2: Wait for the start signal before running.
 
 
        var sw = Stopwatch.StartNew();
        Thread.Sleep(Random.Shared.Next(1000, 3000)); // running...
        Console.WriteLine($"  [{Tid}] {name} crosses the finish line after {sw.ElapsedMilliseconds} ms");
 
        // TODO 5.4: Add 'name' to _finishOrder. Several runners may finish
        //           at the same time and List<T> is NOT thread-safe ->
        //           protect the access with _finishLock.
    }
 
    // =================================================================
    //  PART 6 — PING-PONG (semaphores for turn-taking)
    // =================================================================
 
    private const int PingPongRounds = 5;
 
    private static SemaphoreSlim _pingTurn = new(0);
    private static SemaphoreSlim _pongTurn = new(0);
 
    private static void Part6_PingPong()
    {
        Header("Part 6 — Ping-pong");
 
        // TODO 6.2: Initialise the two semaphores so that Ping serves first.
        //           Which one needs 1 permit, which one 0?
        _pingTurn = new SemaphoreSlim(0);
        _pongTurn = new SemaphoreSlim(0);
 
        var ping = new Thread(PingPlayer);
        var pong = new Thread(PongPlayer);
        ping.Start();
        pong.Start();
        ping.Join();
        pong.Join();
 
        Console.WriteLine("Game over.");
    }
 
    // TODO 6.1: Run Part 6 a few times BEFORE changing anything below.
    //           Does the output look like a ping-pong game?
 
    private static void PingPlayer()
    {
        for (int i = 1; i <= PingPongRounds; i++)
        {
            // TODO 6.3: Wait until it is Ping's turn.
 
            Thread.Sleep(Random.Shared.Next(50, 300)); // thinking...
            Console.WriteLine($"  [{Tid}] Ping   ({i})");
 
            // TODO 6.3: Hand the turn over to Pong.
        }
    }
 
    private static void PongPlayer()
    {
        for (int i = 1; i <= PingPongRounds; i++)
        {
            // TODO 6.3: Wait until it is Pong's turn.
 
            Thread.Sleep(Random.Shared.Next(50, 300)); // thinking...
            Console.WriteLine($"  [{Tid}]     Pong ({i})");
 
            // TODO 6.3: Hand the turn over to Ping.
        }
    }
 
    // TODO 6.4 (bonus): Add a third player "Pang" so that the order becomes
    //           Ping -> Pong -> Pang -> Ping -> ... How many semaphores
    //           do you need?
}