{-# LANGUAGE OverloadedStrings, FlexibleInstances, MultiParamTypeClasses,
    GeneralizedNewtypeDeriving #-}

module Database.Redis.Transactions (
    watch, unwatch, multiExec,
    Queued(), TxResult(..), RedisTx(),
) where

import Control.Applicative
import Control.Monad.State.Strict
import Data.ByteString (ByteString)
import Data.Vector (Vector, fromList, (!))

import Database.Redis.Core
import Database.Redis.Protocol
import Database.Redis.Types


-- |Command-context inside of MULTI\/EXEC transactions. Use 'multiExec' to run
--  actions of this type.
--
--  In the 'RedisTx' context, all commands return a 'Queued' value. It is a
--  proxy object for the /actual/ result, which will only be available after
--  finishing the transaction.
newtype RedisTx a = RedisTx (StateT Int Redis a)
    deriving (Monad, MonadIO, Functor, Applicative)

runRedisTx :: RedisTx a -> Redis a
runRedisTx (RedisTx r) = evalStateT r 0

instance MonadRedis RedisTx where
    liftRedis = RedisTx . lift

instance RedisCtx RedisTx Queued where
    returnDecode _queued = RedisTx $ do
        -- future index in EXEC result list
        i <- get
        put (i+1)
        return $ Queued (decode . (!i))

-- |A 'Queued' value represents the result of a command inside a transaction. It
--  is a proxy object for the /actual/ result, which will only be available
--  after returning from a 'multiExec' transaction.
--
--  'Queued' values are composable by utilizing the 'Functor', 'Applicative' or
--  'Monad' interfaces.
data Queued a = Queued (Vector Reply -> Either Reply a)

instance Functor Queued where
    fmap f (Queued g) = Queued (fmap f . g)

instance Applicative Queued where
    pure x                = Queued (const $ Right x)
    Queued f <*> Queued x = Queued $ \rs -> do
                                        f' <- f rs
                                        x' <- x rs
                                        return (f' x')

instance Monad Queued where
    return         = pure
    Queued x >>= f = Queued $ \rs -> do
                                x' <- x rs
                                let Queued f' = f x'
                                f' rs

-- | Result of a 'multiExec' transaction.
data TxResult a
    = TxSuccess a
    -- ^ Transaction completed successfully. The wrapped value corresponds to
    --   the 'Queued' value returned from the 'multiExec' argument action.
    | TxAborted
    -- ^ Transaction aborted due to an earlier 'watch' command.
    | TxError String
    -- ^ At least one of the commands returned an 'Error' reply.
    deriving (Show, Eq)

-- |Watch the given keys to determine execution of the MULTI\/EXEC block
--  (<http://redis.io/commands/watch>).
watch
    :: [ByteString] -- ^ key
    -> Redis (Either Reply Status)
watch key = sendRequest ("WATCH" : key)

-- |Forget about all watched keys (<http://redis.io/commands/unwatch>).
unwatch :: Redis (Either Reply Status)
unwatch  = sendRequest ["UNWATCH"]


-- |Run commands inside a transaction. For documentation on the semantics of
--  Redis transaction see <http://redis.io/topics/transactions>.
--
--  Inside the transaction block, command functions return their result wrapped
--  in a 'Queued'. The 'Queued' result is a proxy object for the actual
--  command\'s result, which will only be available after @EXEC@ing the
--  transaction.
--
--  Example usage (note how 'Queued' \'s 'Applicative' instance is used to
--  combine the two individual results):
--
--  @
--  runRedis conn $ do
--      set \"hello\" \"hello\"
--      set \"world\" \"world\"
--      helloworld <- 'multiExec' $ do
--          hello <- get \"hello\"
--          world <- get \"world\"
--          return $ (,) \<$\> hello \<*\> world
--      liftIO (print helloworld)
--  @
multiExec :: RedisTx (Queued a) -> Redis (TxResult a)
multiExec rtx = do
    -- We don't need to catch exceptions and call DISCARD. The pool will close
    -- the connection anyway.
    _        <- multi
    Queued f <- runRedisTx rtx
    r        <- exec
    case r of
        MultiBulk rs ->
            return $ maybe
                TxAborted
                (either (TxError . show) TxSuccess . f . fromList)
                rs
        _ -> error $ "hedis: EXEC returned " ++ show r

multi :: Redis (Either Reply Status)
multi = sendRequest ["MULTI"]

exec :: Redis Reply
exec = either id id <$> sendRequest ["EXEC"]