{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Database.Redis.Internal (
    HostName,PortID(..), defaultPort,
    RedisConn(), connect, disconnect,
    Redis(),runRedis,
    send,
    recv,
    sendRequest
) where

import Control.Applicative
import Control.Monad.RWS
import Control.Concurrent
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Network (HostName, PortID(..), connectTo)
import System.IO (Handle, hFlush, hClose, hIsOpen)

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

------------------------------------------------------------------------------
-- Connection
--

-- |Connection to a Redis server. Use the 'connect' function to create one.
--
--  A 'RedisConn' can only be used by a single thread at a time. This means that
--  calls to 'runRedis' or 'disconnet' may block when the 'RedisConn' is shared
--  between multiple threads.
data RedisConn = Conn (MVar (Handle, [Reply]))

withConn :: RedisConn
         -> (Handle -> [Reply] -> IO ([Reply], a))
         -> IO a
withConn (Conn conn) f = do
    (h,rs)     <- takeMVar conn
    (rs',a) <- f h rs
    putMVar conn (h,rs')
    return a

-- |Opens a connection to a Redis server at the given host and port.
connect :: HostName -> PortID -> IO RedisConn
connect host port = do
    h       <- connectTo host port
    replies <- parseReply <$> LB.hGetContents h
    Conn <$> newMVar (h, replies)

-- |Close the given connection.
--
--  May block when the given 'RedisConn' is shared between multiple threads. The
-- 'RedisConn' can not be re-used.
disconnect :: RedisConn -> IO ()
disconnect conn = withConn conn $ \h rs -> do
    open <- hIsOpen h
    when open (hClose h)
    return (rs, ())

-- | The Redis default port 6379. Equivalent to @'PortNumber' 6379@.
defaultPort :: PortID
defaultPort = PortNumber 6379

------------------------------------------------------------------------------
-- The Redis Monad
--
newtype Redis a = Redis (RWST Handle () [Reply] IO a)
    deriving (Monad, MonadIO, Functor, Applicative)

-- |Interact with a Redis datastore specified by the given 'RedisConn'.
--
--  May block when the given 'RedisConn' is shared between multiple threads.
runRedis :: RedisConn -> Redis a -> IO a
runRedis conn (Redis redis) = withConn conn $ \h rs -> do
    open <- hIsOpen h
    if open
        then do
            (a,rs',_) <- runRWST redis h rs
            return (rs',a)
        else error "Redis: disconnected"

send :: [B.ByteString] -> Redis ()
send req = Redis $ do
    h <- ask
    liftIO $ do
        B.hPut h $ renderRequest req
        hFlush h

recv :: Redis Reply
recv = Redis $ do
    -- head/tail avoids forcing the ':' constructor, enabling automatic
    -- pipelining.
    rs <- get
    put (tail rs)
    return (head rs)

-- |Sends a request to the Redis server, returning the 'decode'd reply.
sendRequest :: (RedisResult a) => [B.ByteString] -> Redis (Either Reply a)
sendRequest req = decode <$> (send req >> recv)