{-# LANGUAGE TemplateHaskell #-}
module Polysemy.Final.IO.Internal where

import Data.Functor.Compose
import Data.Maybe

import Control.Concurrent.MVar
import Control.Exception
import Control.Monad
import Control.Monad.Trans.Maybe

import Polysemy
import Polysemy.Final
import Polysemy.Internal
import Polysemy.Internal.Union
import Polysemy.Internal.Strategy

------------------------------------------------------------------------------
-- | Like 'interpretFinal' specialized to 'IO', but also tries very hard
-- to preserve state semantics dependant on the order interpreters are run,
-- adressing the primary issue with 'Final'.
--
-- Semantically, interpreters written using this behave very much as
-- though they were written using 'withLowerToIO'.
-- However, this does not need to spawn an interpreter thread, making
-- it more efficient (but not any more safe.)
--
-- 'interpretFinalGlobal' operates under the assumption that any effectful
-- state which can't be inspected using 'Polysemy.Inspector' can't contain any
-- values. For example, the effectful state for 'Polysemy.runError' is
-- @'Either' e a@. The inspector for this effectful state only fails if the
-- effectful state is a @'Left'@ value, which therefore doesn't contain any
-- values of @a@.
--
-- The assumption holds true for all interpreters featured in polysemy,
-- and is presumably always true for any properly implemented interpreter.
-- 'interpretFinalGlobal' may throw an exception if it is used together with an
-- interpreter that uses 'Polysemy.Internal.Union.weave' improperly.
interpretFinalGlobal
    :: forall e a r
     . Member (Final IO) r
    => (forall x n. e n x -> Strategic IO n x)
    -> Sem (e ': r) a
    -> Sem r a
interpretFinalGlobal f sem = withWeavingToFinal $ \s wv ins -> do
  st  <- newMVar s
  res <- runMaybeT $ runViaFinalGlobal st wv ins f sem
  s'  <- readMVar st
  return (fromMaybe bomb res <$ s')
{-# INLINE interpretFinalGlobal #-}

runViaFinalGlobal :: (Member (Final IO) r, Functor f)
                  => MVar (f ())
                  -> (forall x. f (Sem r x) -> IO (f x))
                  -> (forall x. f x -> Maybe x)
                  -> ( forall x n
                     . e n x
                    -> Strategic IO n x
                     )
                  -> Sem (e ': r) a
                  -> MaybeT IO a
runViaFinalGlobal st wv ins f = usingSem $ \u -> case decomp u of
  Right (Weaving e s' wv' ex ins') ->
    fmap ex $ MaybeT $ fmap getCompose $ runStrategy (f e)
          (Compose (Just s'))
          (  maybe
              (pure (Compose Nothing))
              (  fmap Compose
               . runMaybeT
               . runViaFinalGlobal st wv ins f
               . wv'
              )
           . getCompose
          )
          (getCompose >=> ins')
  Left g -> case prj g of
      Just (Weaving (WithWeavingToFinal wav) s' wv' ex' ins') ->
        MaybeT $ fmap (fmap ex' . getCompose) $
          wav
            (Compose (Just s'))
            (  maybe
                (pure (Compose Nothing))
                ( fmap Compose
                . runMaybeT
                . runViaFinalGlobal st wv ins f
                . wv'
                )
             . getCompose
            )
            (getCompose >=> ins')
      _ -> MaybeT $ mask $ \restore -> do
        -- TODO(KingoftheHomeless): Figure out a solution to polysemy issue #205.
        -- Although we're using a different mechanism, the exact same problem manifests
        -- here.
        s   <- takeMVar st
        res <- restore (wv (liftSem (hoist (interpretFinalGlobal f) g) <$ s))
          `onException` putMVar st s
        putMVar st (() <$ res)
        return $ ins res
{-# INLINE runViaFinalGlobal #-}

bomb :: a
bomb = error
  "interpretFinalGlobal: Uninspectable functorial state \
                        \still carried a result. You're likely using an interpreter \
                        \that uses 'weave' improperly. \
                        \See documentation for more information."