{-# LANGUAGE CPP #-}
module Countdown where

import Control.Monad.ST
import Data.STRef

-- eff
#ifdef VERSION_eff
import Control.Effect qualified as L
#endif

-- cleff
#ifdef VERSION_cleff
import Cleff qualified as C
import Cleff.Reader qualified as C
import Cleff.State qualified as C
#endif

-- effectful
import Effectful qualified as E
import Effectful.Dispatch.Dynamic qualified as E
import Effectful.Labeled qualified as E
import Effectful.Reader.Static qualified as E
import Effectful.State.Dynamic qualified as ED
import Effectful.State.Static.Local qualified as EL
import Effectful.State.Static.Shared qualified as ES

-- freer-simple
#ifdef VERSION_freer_simple
import Control.Monad.Freer qualified as FS
import Control.Monad.Freer.Reader qualified as FS
import Control.Monad.Freer.State qualified as FS
#endif

-- fused-effects
#ifdef VERSION_fused_effects
import Control.Algebra qualified as FE
import Control.Carrier.Reader qualified as FE
import Control.Carrier.State.Strict qualified as FE
#endif

-- mtl
#ifdef VERSION_mtl
import Control.Monad.Reader qualified as M
import Control.Monad.State qualified as M
import Data.Functor.Identity
#endif

-- polysemy
#ifdef VERSION_polysemy
import Polysemy qualified as P
import Polysemy.Reader qualified as P
import Polysemy.State qualified as P
#endif

----------------------------------------
-- reference

countdownRef :: Integer -> (Integer, Integer)
countdownRef n = if n <= 0 then (n, n) else countdownRef $ n - 1
{-# NOINLINE countdownRef #-}

----------------------------------------
-- ST

programST :: STRef s Integer -> ST s Integer
programST ref = do
  n <- readSTRef ref
  if n <= 0
    then pure n
    else do
      writeSTRef ref $! n - 1
      programST ref
{-# NOINLINE programST #-}

countdownST :: Integer -> (Integer, Integer)
countdownST n = runST $ do
  ref <- newSTRef n
  a <- programST ref
  s <- readSTRef ref
  pure (a, s)

----------------------------------------
-- mtl

#ifdef VERSION_mtl

programMtl :: M.MonadState Integer m => m Integer
programMtl = do
  n <- M.get @Integer
  if n <= 0
    then pure n
    else do
      M.put (n - 1)
      programMtl
{-# NOINLINE programMtl #-}

countdownMtlTransformers :: Integer -> (Integer, Integer)
countdownMtlTransformers n = flip M.runState n $ programMtl

countdownMtlTransformersDeep :: Integer -> (Integer, Integer)
countdownMtlTransformersDeep n = runIdentity
  . runR . runR . runR . runR . runR
  . flip M.runStateT n
  . runR . runR . runR . runR . runR
  $ programMtl
  where
    runR = flip M.runReaderT ()

countdownMtlEffectful :: Integer -> (Integer, Integer)
countdownMtlEffectful n = E.runPureEff . ED.runStateLocal n $ programMtl

countdownMtlEffectfulDeep :: Integer -> (Integer, Integer)
countdownMtlEffectfulDeep n = E.runPureEff
  . runR . runR . runR . runR . runR
  . ED.runStateLocal n
  . runR . runR . runR . runR . runR
  $ programMtl
  where
    runR = E.runReader ()

#endif

----------------------------------------
-- effectful (pure)

programEffectfulLocal :: EL.State Integer E.:> es => E.Eff es Integer
programEffectfulLocal = do
  n <- EL.get @Integer
  if n <= 0
    then pure n
    else do
      EL.put (n - 1)
      programEffectfulLocal
{-# NOINLINE programEffectfulLocal #-}

countdownEffectfulLocal :: Integer -> (Integer, Integer)
countdownEffectfulLocal n = E.runPureEff . EL.runState n $ programEffectfulLocal

countdownEffectfulLocalDeep :: Integer -> (Integer, Integer)
countdownEffectfulLocalDeep n = E.runPureEff
  . runR . runR . runR . runR . runR
  . EL.runState n
  . runR . runR . runR . runR . runR
  $ programEffectfulLocal
  where
    runR = E.runReader ()

----

programEffectfulLocalSt :: EL.State Integer E.:> es => E.Eff es Integer
programEffectfulLocalSt = do
  n <- EL.state @Integer $ \s -> (s, s - 1)
  if n <= 0
    then pure n
    else programEffectfulLocalSt
{-# NOINLINE programEffectfulLocalSt #-}

countdownEffectfulLocalSt :: Integer -> (Integer, Integer)
countdownEffectfulLocalSt n = E.runPureEff . EL.runState n $ programEffectfulLocalSt

countdownEffectfulLocalDeepSt :: Integer -> (Integer, Integer)
countdownEffectfulLocalDeepSt n = E.runPureEff
  . runR . runR . runR . runR . runR
  . EL.runState n
  . runR . runR . runR . runR . runR
  $ programEffectfulLocalSt
  where
    runR = E.runReader ()

----

programEffectfulLocalStM :: EL.State Integer E.:> es => E.Eff es Integer
programEffectfulLocalStM = do
  n <- EL.stateM @Integer $ \s -> pure (s, s - 1)
  if n <= 0
    then pure n
    else programEffectfulLocalStM
{-# NOINLINE programEffectfulLocalStM #-}

countdownEffectfulLocalStM :: Integer -> (Integer, Integer)
countdownEffectfulLocalStM n = E.runPureEff . EL.runState n $ programEffectfulLocalStM

countdownEffectfulLocalDeepStM :: Integer -> (Integer, Integer)
countdownEffectfulLocalDeepStM n = E.runPureEff
  . runR . runR . runR . runR . runR
  . EL.runState n
  . runR . runR . runR . runR . runR
  $ programEffectfulLocalStM
  where
    runR = E.runReader ()

----------------------------------------
-- effectful (mvar)

programEffectfulShared :: ES.State Integer E.:> es => E.Eff es Integer
programEffectfulShared = do
  n <- ES.get @Integer
  if n <= 0
    then pure n
    else do
      ES.put (n - 1)
      programEffectfulShared
{-# NOINLINE programEffectfulShared #-}

countdownEffectfulShared :: Integer -> (Integer, Integer)
countdownEffectfulShared n = E.runPureEff . ES.runState n $ programEffectfulShared

countdownEffectfulSharedDeep :: Integer -> (Integer, Integer)
countdownEffectfulSharedDeep n = E.runPureEff
  . runR . runR . runR . runR . runR
  . ES.runState n
  . runR . runR . runR . runR . runR
  $ programEffectfulShared
  where
    runR = E.runReader ()

----------------------------------------
-- effectful (dynamic)

programEffectfulDynamic :: ED.State Integer E.:> es => E.Eff es Integer
programEffectfulDynamic = do
  n <- ED.get @Integer
  if n <= 0
    then pure n
    else do
      ED.put (n - 1)
      programEffectfulDynamic
{-# NOINLINE programEffectfulDynamic #-}

countdownEffectfulDynLocal :: Integer -> (Integer, Integer)
countdownEffectfulDynLocal n =
  E.runPureEff . ED.runStateLocal n $ programEffectfulDynamic

countdownEffectfulDynShared :: Integer -> (Integer, Integer)
countdownEffectfulDynShared n =
  E.runPureEff . ED.runStateShared n $ programEffectfulDynamic

countdownEffectfulDynLocalDeep :: Integer -> (Integer, Integer)
countdownEffectfulDynLocalDeep n = E.runPureEff
  . runR . runR . runR . runR . runR
  . ED.runStateLocal n
  . runR . runR . runR . runR . runR
  $ programEffectfulDynamic
  where
    runR = E.runReader ()

countdownEffectfulDynSharedDeep :: Integer -> (Integer, Integer)
countdownEffectfulDynSharedDeep n = E.runPureEff
  . runR . runR . runR . runR . runR
  . ED.runStateShared n
  . runR . runR . runR . runR . runR
  $ programEffectfulDynamic
  where
    runR = E.runReader ()

----------------------------------------
-- efectful (double-dynamic)

runDoubleStateLocal :: s -> E.Eff (ED.State s : es) a -> E.Eff es (a, s)
runDoubleStateLocal s0 = E.reinterpret (ED.runStateLocal s0) $ \env -> \case
  ED.Get      -> ED.get
  ED.Put s    -> ED.put s
  ED.State f  -> ED.state f
  ED.StateM f -> E.localSeqUnlift env $ \unlift -> ED.stateM (unlift . f)

countdownEffectfulDoubleDynLocal :: Integer -> (Integer, Integer)
countdownEffectfulDoubleDynLocal n =
  E.runPureEff . runDoubleStateLocal n $ programEffectfulDynamic

countdownEffectfulDoubleDynLocalDeep :: Integer -> (Integer, Integer)
countdownEffectfulDoubleDynLocalDeep n = E.runPureEff
  . runR . runR . runR . runR . runR
  . runDoubleStateLocal n
  . runR . runR . runR . runR . runR
  $ programEffectfulDynamic
  where
    runR = E.runReader ()

runDoubleStateShared :: s -> E.Eff (ED.State s : es) a -> E.Eff es (a, s)
runDoubleStateShared s0 = E.reinterpret (ED.runStateShared s0) $ \env -> \case
  ED.Get      -> ED.get
  ED.Put s    -> ED.put s
  ED.State f  -> ED.state f
  ED.StateM f -> E.localSeqUnlift env $ \unlift -> ED.stateM (unlift . f)

countdownEffectfulDoubleDynShared :: Integer -> (Integer, Integer)
countdownEffectfulDoubleDynShared n =
  E.runPureEff . runDoubleStateShared n $ programEffectfulDynamic

countdownEffectfulDoubleDynSharedDeep :: Integer -> (Integer, Integer)
countdownEffectfulDoubleDynSharedDeep n = E.runPureEff
  . runR . runR . runR . runR . runR
  . runDoubleStateShared n
  . runR . runR . runR . runR . runR
  $ programEffectfulDynamic
  where
    runR = E.runReader ()

----------------------------------------
-- effectful (labeled-dynamic-send)

programEffectfulLabeledDynamicSend
  :: E.Labeled "s" (ED.State Integer) E.:> es
  => E.Eff es Integer
programEffectfulLabeledDynamicSend = do
  n <- E.send . E.Labeled @"s" $ ED.Get @Integer
  if n <= 0
    then pure n
    else do
      E.send . E.Labeled @"s" $ ED.Put (n - 1)
      programEffectfulLabeledDynamicSend
{-# NOINLINE programEffectfulLabeledDynamicSend #-}

countdownEffectfulLabeledDynSendLocal :: Integer -> (Integer, Integer)
countdownEffectfulLabeledDynSendLocal n =
  E.runPureEff . E.runLabeled @"s" (ED.runStateLocal n) $ programEffectfulLabeledDynamicSend

countdownEffectfulLabeledDynSendShared :: Integer -> (Integer, Integer)
countdownEffectfulLabeledDynSendShared n =
  E.runPureEff . E.runLabeled @"s" (ED.runStateShared n) $ programEffectfulLabeledDynamicSend

countdownEffectfulLabeledDynSendLocalDeep :: Integer -> (Integer, Integer)
countdownEffectfulLabeledDynSendLocalDeep n = E.runPureEff
  . runR . runR . runR . runR . runR
  . E.runLabeled @"s" (ED.runStateLocal n)
  . runR . runR . runR . runR . runR
  $ programEffectfulLabeledDynamicSend
  where
    runR = E.runReader ()

countdownEffectfulLabeledDynSendSharedDeep :: Integer -> (Integer, Integer)
countdownEffectfulLabeledDynSendSharedDeep n = E.runPureEff
  . runR . runR . runR . runR . runR
  . E.runLabeled @"s" (ED.runStateShared n)
  . runR . runR . runR . runR . runR
  $ programEffectfulLabeledDynamicSend
  where
    runR = E.runReader ()

----------------------------------------
-- effectful (labeled-dynamic)

programEffectfulLabeledDynamic
  :: E.Labeled "s" (ED.State Integer) E.:> es
  => E.Eff es Integer
programEffectfulLabeledDynamic = do
  n <- E.labeled @"s" @(ED.State Integer) $ ED.get @Integer
  if n <= 0
    then pure n
    else do
      E.labeled @"s" @(ED.State Integer) $ ED.put (n - 1)
      programEffectfulLabeledDynamic
{-# NOINLINE programEffectfulLabeledDynamic #-}

countdownEffectfulLabeledDynLocal :: Integer -> (Integer, Integer)
countdownEffectfulLabeledDynLocal n =
  E.runPureEff . E.runLabeled @"s" (ED.runStateLocal n) $ programEffectfulLabeledDynamic

countdownEffectfulLabeledDynShared :: Integer -> (Integer, Integer)
countdownEffectfulLabeledDynShared n =
  E.runPureEff . E.runLabeled @"s" (ED.runStateShared n) $ programEffectfulLabeledDynamic

countdownEffectfulLabeledDynLocalDeep :: Integer -> (Integer, Integer)
countdownEffectfulLabeledDynLocalDeep n = E.runPureEff
  . runR . runR . runR . runR . runR
  . E.runLabeled @"s" (ED.runStateLocal n)
  . runR . runR . runR . runR . runR
  $ programEffectfulLabeledDynamic
  where
    runR = E.runReader ()

countdownEffectfulLabeledDynSharedDeep :: Integer -> (Integer, Integer)
countdownEffectfulLabeledDynSharedDeep n = E.runPureEff
  . runR . runR . runR . runR . runR
  . E.runLabeled @"s" (ED.runStateShared n)
  . runR . runR . runR . runR . runR
  $ programEffectfulLabeledDynamic
  where
    runR = E.runReader ()

----------------------------------------
-- fused-effects

#ifdef VERSION_fused_effects

programFusedEffects :: FE.Has (FE.State Integer) sig m => m Integer
programFusedEffects = do
  n <- FE.get @Integer
  if n <= 0
    then pure n
    else do
      FE.put (n - 1)
      programFusedEffects
{-# NOINLINE programFusedEffects #-}

countdownFusedEffects :: Integer -> (Integer, Integer)
countdownFusedEffects n = FE.run . FE.runState n $ programFusedEffects

countdownFusedEffectsDeep :: Integer -> (Integer, Integer)
countdownFusedEffectsDeep n = FE.run
  . runR . runR . runR . runR . runR
  . FE.runState n
  . runR . runR . runR . runR . runR
  $ programFusedEffects
  where
    runR = FE.runReader ()

#endif

----------------------------------------
-- polysemy

#ifdef VERSION_polysemy

programPolysemy :: P.Member (P.State Integer) r => P.Sem r Integer
programPolysemy = do
  n <- P.get @Integer
  if n <= 0
    then pure n
    else do
      P.put (n - 1)
      programPolysemy
{-# NOINLINE programPolysemy #-}

countdownPolysemy :: Integer -> (Integer, Integer)
countdownPolysemy n = P.run . P.runState n $ programPolysemy

countdownPolysemyDeep :: Integer -> (Integer, Integer)
countdownPolysemyDeep n = P.run
  . runR . runR . runR . runR . runR
  . P.runState n
  . runR . runR . runR . runR . runR
  $ programPolysemy
  where
    runR = P.runReader ()

#endif

----------------------------------------
-- eff

#ifdef VERSION_eff

programEff :: L.State Integer L.:< es => L.Eff es Integer
programEff = do
  n <- L.get @Integer
  if n <= 0
    then pure n
    else do
      L.put (n - 1)
      programEff
{-# NOINLINE programEff #-}

countdownEff :: Integer -> (Integer, Integer)
countdownEff n = L.run . L.runState n $ programEff

countdownEffDeep :: Integer -> (Integer, Integer)
countdownEffDeep n = L.run
  . runR . runR . runR . runR . runR
  . L.runState n
  . runR . runR . runR . runR . runR
  $ programEff
  where
    runR = L.runReader ()

#endif

----------------------------------------
-- cleff

#ifdef VERSION_cleff

programCleff :: C.State Integer C.:> es => C.Eff es Integer
programCleff = do
  n <- C.get @Integer
  if n <= 0
    then pure n
    else do
      C.put (n - 1)
      programCleff
{-# NOINLINE programCleff #-}

countdownCleffLocal :: Integer -> (Integer, Integer)
countdownCleffLocal n = C.runPure . C.runStateLocal n $ programCleff

countdownCleffLocalDeep :: Integer -> (Integer, Integer)
countdownCleffLocalDeep n = C.runPure
  . runR . runR . runR . runR . runR
  . C.runStateLocal n
  . runR . runR . runR . runR . runR
  $ programCleff
  where
    runR = C.runReader ()

countdownCleffIORef :: Integer -> (Integer, Integer)
countdownCleffIORef n = C.runPure . C.runState n $ programCleff

countdownCleffIORefDeep :: Integer -> (Integer, Integer)
countdownCleffIORefDeep n = C.runPure
  . runR . runR . runR . runR . runR
  . C.runState n
  . runR . runR . runR . runR . runR
  $ programCleff
  where
    runR = C.runReader ()

#endif

----------------------------------------
-- freer-simple

#ifdef VERSION_freer_simple

programFreerSimple :: FS.Member (FS.State Integer) es => FS.Eff es Integer
programFreerSimple = do
  n <- FS.get @Integer
  if n <= 0
    then pure n
    else do
      FS.put (n - 1)
      programFreerSimple
{-# NOINLINE programFreerSimple #-}

countdownFreerSimple :: Integer -> (Integer, Integer)
countdownFreerSimple n = FS.run . FS.runState n $ programFreerSimple

countdownFreerSimpleDeep :: Integer -> (Integer, Integer)
countdownFreerSimpleDeep n = FS.run
  . runR . runR . runR . runR . runR
  . FS.runState n
  . runR . runR . runR . runR . runR
  $ programFreerSimple
  where
    runR = FS.runReader ()

#endif
