-- | Run a simulation.
simulate :: SimulationParameters -> IO SimulationOutput
simulate SimulationParameters{..} = do

    let Scenario{..} = sim_scenario

    -- All threads will wait on this.
    -- Threads will loop, and at each iteration will read it. If it's True,
    -- they'll stop.
    startStop :: MVar Bool <- newEmptyMVar

    -- Spawn the reader.
    refs <- withAsync (reader 0 sim_reader) $ \readerThread -> do
        -- Spawn the writers.
        writerThreadsAndRefs <- forM (zip [1..] sim_writers) $ \(seed', sim_writer) -> do
            (ref, doWrites) <- writer startStop seed' sim_writer
            --withAsync doWrites $ \thread -> return (ref, thread)
            thread <- async doWrites
            return (ref, thread)
        -- Duration is in seconds.
        putMVar startStop False
        threadDelay $ fromIntegral sim_duration * 1000000
        swapMVar startStop True
        forM writerThreadsAndRefs $ \(ref, thread) -> do
            wait thread
            return ref

    -- Reader and writers are all killed. Results available in the IORefs.
    vectors <- forM refs (fmap fromList . readIORef)

    return $ SimulationOutput ((fmap . fmap) fromIntegral vectors)

    where

    reader :: Word32 -> SimulationReader -> IO ()
    reader seed' (SimulationReader distribution) = do
        gen <- initialize (fromList [sim_seed, seed'])
        let readLoop = do
                () <- qdiscDequeue sim_qdisc
                -- Unlike for writers, the reader delays are always respected,
                -- as the delay is independent of how long the reader waits for
                -- a value from qdiscDequeue.
                delay :: Microsecond <- fromIntegral . round <$> genContVar distribution gen
                threadDelay (fromIntegral delay)
                readLoop
        readLoop

    writer :: MVar Bool -> Word32 -> SimulationWriter -> IO (IORef [Microsecond], IO ())
    writer control seed' (SimulationWriter distribution) = do
        ref <- newIORef []
        gen <- initialize (fromList [sim_seed, seed'])
        -- The writer's distribution determines the duration between the points
        -- in time when new data are made available, and this is independent
        -- of how long it takes to actually do the write. For instance, if
        -- the writer is blocked on enqueue for 2 seconds, and the distribution
        -- says there will be data available every 1 second, then 2 data will
        -- be immediately available after the enqueue finishes.
        let writeLoop :: Microsecond -> IO ()
            writeLoop surplusWait = do
                stop <- readMVar control
                unless stop $ do
                    nextDelay :: Microsecond <- fromIntegral . round <$> genContVar distribution gen
                    let actualDelay = nextDelay - surplusWait
                    when (actualDelay > 0) $ do
                        threadDelay (fromIntegral actualDelay)
                    start <- getPOSIXTime
                    qdiscEnqueue sim_qdisc undefined undefined ()
                    end <- getPOSIXTime
                    let latency :: Microsecond
                        !latency = fromIntegral . round $ (realToFrac (end - start) :: Double) * 1000000
                    modifyIORef' ref ((:) latency)
                    let !surplusWait' = max 0 (surplusWait + latency - nextDelay)
                    writeLoop surplusWait'
        return (ref, writeLoop 0)
simulate :: SimulationParameters -> IO SimulationOutput
simulate SimulationParameters{..} = do

    let Scenario{..} = sim_scenario

    -- All threads will wait on this.
    -- Threads will loop, and at each iteration will read it. If it's True,
    -- they'll stop.
    startStop :: MVar Bool <- newEmptyMVar

    -- Spawn the reader.
    refs <- withAsync (reader 0 sim_reader) $ \readerThread -> do
        -- Spawn the writers.
        writerThreadsAndRefs <- forM (zip [1..] sim_writers) $ \(seed', sim_writer) -> do
            (ref, doWrites) <- writer startStop seed' sim_writer
            --withAsync doWrites $ \thread -> return (ref, thread)
            thread <- async doWrites
            return (ref, thread)
        -- Duration is in seconds.
        putMVar startStop False
        threadDelay $ fromIntegral sim_duration * 1000000
        swapMVar startStop True
        forM writerThreadsAndRefs $ \(ref, thread) -> do
            wait thread
            return ref

    -- Reader and writers are all killed. Results available in the IORefs.
    vectors <- forM refs (fmap fromList . readIORef)

    return $ SimulationOutput ((fmap . fmap) fromIntegral vectors)

    where

    reader :: Word32 -> SimulationReader -> IO ()
    reader seed' (SimulationReader distribution) = do
        gen <- initialize (fromList [sim_seed, seed'])
        let readLoop = do
                () <- qdiscDequeue sim_qdisc
                -- Unlike for writers, the reader delays are always respected,
                -- as the delay is independent of how long the reader waits for
                -- a value from qdiscDequeue.
                delay :: Microsecond <- fromIntegral . round <$> genContVar distribution gen
                threadDelay (fromIntegral delay)
                readLoop
        readLoop

    writer :: MVar Bool -> Word32 -> SimulationWriter -> IO (IORef [Microsecond], IO ())
    writer control seed' (SimulationWriter distribution) = do
        ref <- newIORef []
        gen <- initialize (fromList [sim_seed, seed'])
        -- The writer's distribution determines the duration between the points
        -- in time when new data are made available, and this is independent
        -- of how long it takes to actually do the write. For instance, if
        -- the writer is blocked on enqueue for 2 seconds, and the distribution
        -- says there will be data available every 1 second, then 2 data will
        -- be immediately available after the enqueue finishes.
        let writeLoop :: Microsecond -> IO ()
            writeLoop surplusWait = do
                stop <- readMVar control
                unless stop $ do
                    nextDelay :: Microsecond <- fromIntegral . round <$> genContVar distribution gen
                    let actualDelay = nextDelay - surplusWait
                    when (actualDelay > 0) $ do
                        threadDelay (fromIntegral actualDelay)
                    start <- getPOSIXTime
                    qdiscEnqueue sim_qdisc undefined undefined ()
                    end <- getPOSIXTime
                    let latency :: Microsecond
                        !latency = fromIntegral . round $ (realToFrac (end - start) :: Double) * 1000000
                    modifyIORef' ref ((:) latency)
                    let !surplusWait' = max 0 (surplusWait + latency - nextDelay)
                    writeLoop surplusWait'
        return (ref, writeLoop 0)
simulate SimulationParameters{..} = do

    let Scenario{..} = sim_scenario

    -- All threads will wait on this.
    -- Threads will loop, and at each iteration will read it. If it's True,
    -- they'll stop.
    startStop :: MVar Bool <- newEmptyMVar

    -- Spawn the reader.
    refs <- withAsync (reader 0 sim_reader) $ \readerThread -> do
        -- Spawn the writers.
        writerThreadsAndRefs <- forM (zip [1..] sim_writers) $ \(seed', sim_writer) -> do
            (ref, doWrites) <- writer startStop seed' sim_writer
            --withAsync doWrites $ \thread -> return (ref, thread)
            thread <- async doWrites
            return (ref, thread)
        -- Duration is in seconds.
        putMVar startStop False
        threadDelay $ fromIntegral sim_duration * 1000000
        swapMVar startStop True
        forM writerThreadsAndRefs $ \(ref, thread) -> do
            wait thread
            return ref

    -- Reader and writers are all killed. Results available in the IORefs.
    vectors <- forM refs (fmap fromList . readIORef)

    return $ SimulationOutput ((fmap . fmap) fromIntegral vectors)

    where

    reader :: Word32 -> SimulationReader -> IO ()
    reader seed' (SimulationReader distribution) = do
        gen <- initialize (fromList [sim_seed, seed'])
        let readLoop = do
                () <- qdiscDequeue sim_qdisc
                -- Unlike for writers, the reader delays are always respected,
                -- as the delay is independent of how long the reader waits for
                -- a value from qdiscDequeue.
                delay :: Microsecond <- fromIntegral . round <$> genContVar distribution gen
                threadDelay (fromIntegral delay)
                readLoop
        readLoop

    writer :: MVar Bool -> Word32 -> SimulationWriter -> IO (IORef [Microsecond], IO ())
    writer control seed' (SimulationWriter distribution) = do
        ref <- newIORef []
        gen <- initialize (fromList [sim_seed, seed'])
        -- The writer's distribution determines the duration between the points
        -- in time when new data are made available, and this is independent
        -- of how long it takes to actually do the write. For instance, if
        -- the writer is blocked on enqueue for 2 seconds, and the distribution
        -- says there will be data available every 1 second, then 2 data will
        -- be immediately available after the enqueue finishes.
        let writeLoop :: Microsecond -> IO ()
            writeLoop surplusWait = do
                stop <- readMVar control
                unless stop $ do
                    nextDelay :: Microsecond <- fromIntegral . round <$> genContVar distribution gen
                    let actualDelay = nextDelay - surplusWait
                    when (actualDelay > 0) $ do
                        threadDelay (fromIntegral actualDelay)
                    start <- getPOSIXTime
                    qdiscEnqueue sim_qdisc undefined undefined ()
                    end <- getPOSIXTime
                    let latency :: Microsecond
                        !latency = fromIntegral . round $ (realToFrac (end - start) :: Double) * 1000000
                    modifyIORef' ref ((:) latency)
                    let !surplusWait' = max 0 (surplusWait + latency - nextDelay)
                    writeLoop surplusWait'
        return (ref, writeLoop 0)