{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Database.Redis.Core (
    Connection(..),
    Redis(),runRedis,runRedisInternal,
    send,
    recv,
    sendRequest
) where

import Control.Applicative
import Control.Arrow
import Control.Monad.Reader
import Control.Concurrent
import qualified Data.ByteString as B
import Data.IORef
import Data.Pool
import System.IO (Handle, hFlush)

import Database.Redis.Reply
import Database.Redis.Request
import Database.Redis.Types

-- |Connection to a Redis server. Use the 'connect' function to create one.
--
--  A 'Connection' is actually a pool of network connections.
newtype Connection = Conn (Pool (MVar (Handle, IORef [Reply])))

-- |All Redis commands run in the 'Redis' monad.
newtype Redis a = Redis (ReaderT RedisEnv IO a)
    deriving (Monad, MonadIO, Functor, Applicative)

type RedisEnv = (Handle, IORef [Reply])

askHandle :: ReaderT RedisEnv IO Handle
askHandle = asks fst

askReplies :: ReaderT RedisEnv IO (IORef [Reply])
askReplies = asks snd

-- |Interact with a Redis datastore specified by the given 'Connection'.
--
--  Each call of 'runRedis' takes a network connection from the 'Connection'
--  pool and runs the given 'Redis' action. Calls to 'runRedis' may thus block, --  while all connections from the pool are in use.
runRedis :: Connection -> Redis a -> IO a
runRedis (Conn pool) redis =
    withResource pool $ \conn ->
    withMVar conn $ \conn' -> runRedisInternal conn' redis

-- |Internal version of 'runRedis' that does not depend on the 'Connection'
--  abstraction. Used to run the AUTH command when connecting. 
runRedisInternal :: RedisEnv -> Redis a -> IO a
runRedisInternal env (Redis redis) = runReaderT redis env

send :: [B.ByteString] -> Redis ()
send req = Redis $ do
    h <- askHandle
    liftIO $ do
        {-# SCC "send.hPut" #-} B.hPut h $ renderRequest req
        {-# SCC "send.hFlush" #-} hFlush h

recv :: Redis Reply
recv = Redis $ do
    -- head/tail avoids forcing the ':' constructor, enabling automatic
    -- pipelining.
    rs <- askReplies
    liftIO $ atomicModifyIORef rs (tail &&& head)

sendRequest :: (RedisResult a) => [B.ByteString] -> Redis (Either Reply a)
sendRequest req = decode <$> (send req >> recv)