{-# LANGUAGE BlockArguments #-}

module Hercules.Agent.Worker.STM where

import Control.Concurrent.Async
import Control.Concurrent.STM (TVar, readTVar, writeTVar)
import Control.Monad.Fix (mfix)
import Data.Map qualified as M
import Protolude

asyncIfSTM ::
  -- | Either return an existing async or return @b@ as an input to a new one.
  --
  -- The returned transaction must be not evaluate the 'Async' argument,
  -- but can write it somewhere.
  (Async a -> STM (Either (Async a) b)) ->
  -- | The action to run when the STM transaction decided to create a new 'Async'.
  (b -> IO a) ->
  -- | Either the async returned by the STM transaction, or a new async waiting
  -- for the provided IO.
  IO (Async a)
asyncIfSTM cond io = mfix \lazyAsync ->
  atomically (cond lazyAsync) >>= \case
    Left early -> pure early
    Right b -> do
      async (io b)

ensureTVarMapItem :: (Ord k) => k -> STM v -> TVar (Map k v) -> STM (Bool, v)
ensureTVarMapItem key mkValue mapVar = do
  map0 <- readTVar mapVar
  case M.lookup key map0 of
    Nothing -> do
      value <- mkValue
      writeTVar mapVar (M.insert key value map0)
      pure (False, value)
    Just value0 -> do
      pure (True, value0)

asyncInTVarMap :: (Ord k) => k -> TVar (Map k (Async a)) -> IO a -> IO (Async a)
asyncInTVarMap key mapVar action =
  asyncIfSTM
    ( \asy -> do
        (existed, v) <- ensureTVarMapItem key (pure asy) mapVar
        if existed
          then pure (Left v)
          else pure (Right ())
    )
    (\() -> action)
