// ============================================================================
//  AsyncLab – async / await in C#
// ============================================================================
//  Goal: learn what async/await is mostly used for – waiting for I/O
//  (network, files, databases) WITHOUT blocking a thread – and how Task.Run
//  moves CPU-heavy work off the main thread.
//
//  Setup:  dotnet new console -n AsyncLab
//          replace Program.cs with this file, then: dotnet run
//
//  The program starts a small local web server (LabServer, bottom of the
//  file) that simulates SLOW network services. No internet needed.
//
//  Work through the TODOs in order. After every part: run the program,
//  choose the part in the menu and compare the times with the exercise sheet.
// ============================================================================
using System.Diagnostics;
using System.Net;
 
namespace AsyncLab;
 
public static class Program
{
    // One shared HttpClient for the whole application (best practice!).
    // Relative URLs like "weather/Krems" are resolved against BaseAddress.
    private static readonly HttpClient Http = new() { BaseAddress = new Uri(LabServer.BaseUrl) };
 
    private static readonly string[] Cities = { "Krems", "Wien", "Linz", "Graz", "Salzburg" };
 
    public static async Task Main()
    {
        LabServer.Start();
 
        while (true)
        {
            Console.WriteLine();
            Console.WriteLine("===== AsyncLab =====");
            Console.WriteLine(" 1  Sequential downloads        (await one after another)");
            Console.WriteLine(" 2  Concurrent downloads        (Task.WhenAll)");
            Console.WriteLine(" 3  Fastest mirror & timeout    (Task.WhenAny, CancellationToken)");
            Console.WriteLine(" 4  CPU-bound work              (Task.Run, responsive UI)");
            Console.WriteLine(" 5  Parallel speed-up           (many Task.Run + Task.WhenAll)");
            Console.WriteLine(" 0  Exit");
            Console.Write("Choose a part: ");
 
            switch (Console.ReadLine()?.Trim())
            {
                case "1": await Part1_SequentialAsync(); break;
                case "2": await Part2_ConcurrentAsync(); break;
                case "3": await Part3_WhenAnyAsync(); break;
                case "4": await Part4_CpuBoundAsync(); break;
                case "5": await Part5_ParallelAsync(); break;
                case "0": return;
                default: Console.WriteLine("Unknown option."); break;
            }
        }
    }
 
    // ------------------------------------------------------------------------
    // PART 1 – Sequential downloads
    // Each request to /weather/{city} takes about 1 second.
    // ------------------------------------------------------------------------
    private static async Task Part1_SequentialAsync()
    {
        Console.WriteLine("\n--- Part 1: sequential downloads ---");
        var sw = Stopwatch.StartNew();
 
        foreach (string city in Cities)
        {
            // TODO 1: Download the weather report for `city` from the URL
            //         $"weather/{city}" using Http.GetStringAsync(...).
            //         Await the result and store it in `report`.
            string report = "TODO 1";
 
            Console.WriteLine($"  [{sw.ElapsedMilliseconds,5} ms] {report}");
        }
 
        Console.WriteLine($"Total time: {sw.ElapsedMilliseconds} ms");
    }
 
    // ------------------------------------------------------------------------
    // PART 2 – Concurrent downloads with Task.WhenAll
    // Same 5 requests – but this time we do not wait for each one before
    // starting the next.
    // ------------------------------------------------------------------------
    private static async Task Part2_ConcurrentAsync()
    {
        Console.WriteLine("\n--- Part 2: concurrent downloads ---");
        var sw = Stopwatch.StartNew();
 
        // TODO 2a: For every city, START the download (Http.GetStringAsync)
        //          but do NOT await it. Add the returned Task<string> to `tasks`.
        var tasks = new List<Task<string>>();
 
        Console.WriteLine($"  [{sw.ElapsedMilliseconds,5} ms] all {tasks.Count} requests started");
 
        // TODO 2b: Wait for ALL tasks at once using Task.WhenAll(...).
        //          Hint: awaiting Task.WhenAll on Task<string>s gives you a string[].
        string[] reports = Array.Empty<string>();
 
        foreach (string report in reports)
            Console.WriteLine($"  {report}");
 
        Console.WriteLine($"Total time: {sw.ElapsedMilliseconds} ms");
    }
 
    // ------------------------------------------------------------------------
    // PART 3 – Task.WhenAny: fastest mirror, timeout, cancellation
    // The same file is available on /mirror/1, /mirror/2 and /mirror/3,
    // but the mirrors respond at different speeds. /slow takes 5 seconds.
    // ------------------------------------------------------------------------
    private static async Task Part3_WhenAnyAsync()
    {
        Console.WriteLine("\n--- Part 3a: fastest mirror ---");
        var sw = Stopwatch.StartNew();
 
        // TODO 3a: Start a download from each of the three mirrors
        //          ($"mirror/{i}" for i = 1..3) and collect the tasks.
        //          Use Task.WhenAny(...) to get the task that finishes FIRST,
        //          then await that task to get its string.
        string fastest = "TODO 3a";
 
        Console.WriteLine($"  [{sw.ElapsedMilliseconds,5} ms] winner: {fastest}");
 
        Console.WriteLine("\n--- Part 3b: timeout with Task.WhenAny ---");
        sw.Restart();
 
        // TODO 3b: Start a request to "slow" AND a Task.Delay(2000).
        //          Race them with Task.WhenAny. If the delay task wins,
        //          set `timedOut` to true.
        //          Replace the placeholder below with your own slow request task.
        Task<string> slowTask = Task.FromResult("TODO 3b");
        bool timedOut = false;
 
        Console.WriteLine(timedOut
            ? $"  [{sw.ElapsedMilliseconds,5} ms] Timeout! The server took too long."
            : $"  [{sw.ElapsedMilliseconds,5} ms] Answer: {await slowTask}");
 
        Console.WriteLine("\n--- Part 3c (bonus): real cancellation with a CancellationToken ---");
        sw.Restart();
 
        // TODO 3c (BONUS): In 3b the slow request keeps running in the background.
        //          Do it properly: create a CancellationTokenSource that cancels
        //          after 2 seconds and pass its Token to Http.GetStringAsync("slow", token).
        //          Catch the TaskCanceledException and print
        //          "Request cancelled after timeout." together with the elapsed time.
        Console.WriteLine("  TODO 3c");
    }
 
    // ------------------------------------------------------------------------
    // PART 4 – CPU-bound work with Task.Run
    // CountPrimes(...) is pure computation – there is NOTHING to wait for, so
    // there is no "...Async" version. Task.Run moves it to a thread-pool thread.
    // ------------------------------------------------------------------------
    private static async Task Part4_CpuBoundAsync()
    {
        Console.WriteLine("\n--- Part 4: CPU-bound work, UI stays responsive ---");
        const int max = 4_000_000;
        var sw = Stopwatch.StartNew();
 
        // TODO 4a: Run CountPrimes(2, max) in the background with Task.Run(...).
        //          Replace the placeholder (which already "finished" with 0).
        Task<int> primeTask = Task.FromResult(0);
 
        // TODO 4b: While primeTask is NOT completed (primeTask.IsCompleted),
        //          show a spinner ( | / - \ ) and wait 100 ms with
        //          await Task.Delay(100) between the frames.
        //          Tip: Console.Write("\r...") overwrites the current line.
 
        // TODO 4c: Await primeTask to get the result.
        int count = 0;
 
        Console.WriteLine($"\r  Found {count} primes below {max:N0} in {sw.ElapsedMilliseconds} ms");
    }
 
    // ------------------------------------------------------------------------
    // PART 5 – Parallel speed-up with several Task.Run calls
    // ------------------------------------------------------------------------
    private static async Task Part5_ParallelAsync()
    {
        Console.WriteLine("\n--- Part 5: parallel speed-up ---");
        const int max = 8_000_000;
        Console.WriteLine($"  This machine has {Environment.ProcessorCount} logical cores.");
 
        long baseline = 0;
        foreach (int taskCount in new[] { 1, 2, 4, Environment.ProcessorCount }.Distinct())
        {
            var sw = Stopwatch.StartNew();
            int total = await CountPrimesParallelAsync(max, taskCount);
            long ms = sw.ElapsedMilliseconds;
            if (taskCount == 1) baseline = ms;
 
            Console.WriteLine($"  {taskCount,2} task(s): {total} primes in {ms,5} ms" +
                              $"   speed-up: {(double)baseline / Math.Max(ms, 1):F2}x");
        }
    }
 
    private static async Task<int> CountPrimesParallelAsync(int max, int taskCount)
    {
        // TODO 5a: Split the range [0, max) into `taskCount` chunks of equal size.
        //          For each chunk start Task.Run(() => CountPrimes(from, to))
        //          and add the task to `tasks`.
        //          Careful 1: the LAST chunk must end exactly at `max`.
        //          Careful 2: copy from/to into local variables INSIDE the loop
        //                     before using them in the lambda!
        var tasks = new List<Task<int>>();
 
        // TODO 5b: Await all tasks with Task.WhenAll and return the SUM
        //          of all partial results.
        await Task.CompletedTask; // remove this line when you are done
        return 0;
    }
 
    // ------------------------------------------------------------------------
    // Helpers – no changes needed
    // ------------------------------------------------------------------------
 
    /// <summary>Counts primes in [from, to). Pure CPU work, deliberately naive.</summary>
    private static int CountPrimes(int from, int to)
    {
        int count = 0;
        for (int n = Math.Max(from, 2); n < to; n++)
        {
            bool isPrime = true;
            for (int d = 2; (long)d * d <= n; d++)
            {
                if (n % d == 0) { isPrime = false; break; }
            }
            if (isPrime) count++;
        }
        return count;
    }
}
 
// ============================================================================
//  LabServer – a tiny local web server that simulates slow network services.
//  You do NOT need to change anything here (but it is worth a look: it is
//  itself written with async/await!).
//
//    GET /weather/{city}   ~1000 ms   -> weather report for a city
//    GET /mirror/{1|2|3}   2500 / 800 / 1500 ms -> same file from 3 mirrors
//    GET /slow             5000 ms    -> a server that is far too slow
// ============================================================================
public static class LabServer
{
    public const string BaseUrl = "http://localhost:5055/";
    private static HttpListener? _listener;
 
    public static void Start()
    {
        _listener = new HttpListener();
        _listener.Prefixes.Add(BaseUrl);
        _listener.Start();
        _ = AcceptLoopAsync(); // runs in the background
        Console.WriteLine($"LabServer running at {BaseUrl}");
    }
 
    private static async Task AcceptLoopAsync()
    {
        while (_listener is { IsListening: true })
        {
            HttpListenerContext ctx;
            try { ctx = await _listener.GetContextAsync(); }
            catch { break; }
 
            _ = HandleAsync(ctx); // do not await -> many requests at the same time
        }
    }
 
    private static async Task HandleAsync(HttpListenerContext ctx)
    {
        string[] parts = ctx.Request.Url!.AbsolutePath.Trim('/').Split('/');
        (int delayMs, string body) = parts switch
        {
            ["weather", var city] => (1000, Weather(Uri.UnescapeDataString(city))),
            ["mirror", "1"] => (2500, "data.zip from mirror 1 (Frankfurt)"),
            ["mirror", "2"] => (800, "data.zip from mirror 2 (Vienna)"),
            ["mirror", "3"] => (1500, "data.zip from mirror 3 (Zurich)"),
            ["slow"] => (5000, "finally... here is your answer"),
            _ => (0, "404 – unknown endpoint")
        };
 
        try
        {
            await Task.Delay(delayMs); // simulated network / database latency
            byte[] bytes = System.Text.Encoding.UTF8.GetBytes(body);
            ctx.Response.ContentType = "text/plain; charset=utf-8";
            ctx.Response.StatusCode = body.StartsWith("404") ? 404 : 200;
            await ctx.Response.OutputStream.WriteAsync(bytes);
            ctx.Response.Close();
        }
        catch
        {
            // client gave up (e.g. cancellation) – ignore
        }
    }
 
    private static string Weather(string city)
    {
        string[] sky = { "sunny", "cloudy", "rainy", "foggy", "windy" };
        int h = Math.Abs(city.Aggregate(17, (acc, c) => acc * 31 + c));
        return $"{city,-9}: {h % 25 - 3,3} °C, {sky[h % sky.Length]}";
    }
}