-- | Provider of the t'Control.Applicative.Alternative' and
-- t'Control.Monad.MonadPlus' instance for 'Eff'.
module Effectful.NonDet
  ( -- * Effect
    NonDet(..)
  , OnEmptyPolicy(..)

    -- ** Handlers
  , runNonDet

  -- * Operations
  , emptyEff
  , plusEff
  , sumEff

    -- * Re-exports
  , Alternative(..)
  , HasCallStack
  , CallStack
  , getCallStack
  , prettyCallStack
  ) where

import Control.Applicative
import Data.IORef.Strict
import GHC.Generics
import GHC.Stack

import Effectful
import Effectful.Dispatch.Dynamic
import Effectful.Dispatch.Static
import Effectful.Dispatch.Static.Primitive
import Effectful.Error.Static
import Effectful.Internal.Env qualified as I
import Effectful.Internal.Monad (NonDet(..))

-- | Policy of dealing with modifications to __thread local__ state in the
-- environment in branches that end up calling the 'Empty' operation.
--
-- /Note:/ 'OnEmptyKeep' is faster as there is no need to back up the
-- environment on each call to ':<|>:'.
--
-- @since 2.2.0.0
data OnEmptyPolicy
  = OnEmptyKeep
  -- ^ Keep modifications on 'Empty'.
  | OnEmptyRollback
  -- ^ Rollback modifications on 'Empty'.
  --
  -- /Note:/ state modifications are rolled back on 'Empty' only. In particular,
  -- they are __not__ rolled back on exceptions.
  deriving stock (Eq, Generic, Ord, Show)

-- | Run the 'NonDet' effect with a given 'OnEmptyPolicy'.
--
-- /Note:/ ':<|>:' executes the second computation if (and only if) the first
-- computation calls 'Empty'.
--
-- @since 2.2.0.0
runNonDet
  :: HasCallStack
  => OnEmptyPolicy
  -> Eff (NonDet : es) a
  -> Eff es (Either CallStack a)
runNonDet = \case
  OnEmptyKeep     -> runNonDetKeep
  OnEmptyRollback -> runNonDetRollback

runNonDetKeep
  :: HasCallStack
  => Eff (NonDet : es) a
  -> Eff es (Either CallStack a)
runNonDetKeep = reinterpret (fmap noError . runError @ErrorEmpty) $ \env -> \case
  Empty       -> throwError ErrorEmpty
  m1 :<|>: m2 -> localSeqUnlift env $ \unlift -> do
    mr <- (Just <$> unlift m1) `catchError` \_ ErrorEmpty -> pure Nothing
    case mr of
      Just r  -> pure r
      Nothing -> unlift m2

runNonDetRollback
  :: HasCallStack
  => Eff (NonDet : es) a
  -> Eff es (Either CallStack a)
runNonDetRollback = reinterpret setup $ \env -> \case
  Empty       -> throwError ErrorEmpty
  m1 :<|>: m2 -> do
    backupData <- unsafeEff backupStorageData
    localSeqUnlift env $ \unlift -> do
      mr <- (Just <$> unlift m1) `catchError` \_ ErrorEmpty -> do
        -- If m1 failed, restore the data.
        unsafeEff $ I.restoreStorageData backupData
        pure Nothing
      case mr of
        Just r  -> pure r
        Nothing -> unlift m2
  where
    setup action = do
      backupData <- unsafeEff backupStorageData
      runError @ErrorEmpty action >>= \case
        Right r -> pure $ Right r
        Left (cs, _) -> do
          -- If the whole action failed, restore the data.
          unsafeEff $ I.restoreStorageData backupData
          pure $ Left cs

----------------------------------------

-- | Specialized version of 'empty' with the 'HasCallStack' constraint for
-- tracking purposes.
--
-- @since 2.2.0.0
emptyEff :: (HasCallStack, NonDet :> es) => Eff es a
emptyEff = withFrozenCallStack send Empty

-- | Specialized version of '<|>' with the `HasCallStack` constraint for
-- tracking purposes.
--
-- @since 2.5.0.0
plusEff :: (HasCallStack, NonDet :> es) => Eff es a -> Eff es a -> Eff es a
plusEff m1 m2 = send (m1 :<|>: m2)
infixl 3 `plusEff` -- same as <|>

-- | Specialized version of 'asum' with the 'HasCallStack' constraint for
-- tracking purposes.
--
-- @since 2.2.0.0
sumEff :: (HasCallStack, Foldable t, NonDet :> es) => t (Eff es a) -> Eff es a
sumEff = foldr plusEff emptyEff

----------------------------------------
-- Internal helpers

-- | Internal error type for the Empty action. Better than '()' in case it
-- escapes the scope of 'runNonDet' and shows up in error messages.
data ErrorEmpty = ErrorEmpty
instance Show ErrorEmpty where
  show ErrorEmpty = "Effectful.NonDet.ErrorEmpty"

noError :: Either (cs, e) a -> Either cs a
noError = either (Left . fst) Right

backupStorageData :: HasCallStack => Env es -> IO I.StorageData
backupStorageData env = I.copyStorageData . I.stData =<< readIORef' (I.envStorage env)
