{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE FlexibleInstances   #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Snap.Snaplet.PostgresqlSimple.Internal where

import           Control.Monad.IO.Class
import           Control.Monad.Trans
import           Control.Monad.Trans.Control (MonadBaseControl(..), control)
import           Control.Monad.Trans.Identity
import           Control.Monad.Trans.List
import           Control.Monad.Trans.Maybe
import           Control.Monad.Trans.Reader
import qualified Control.Monad.Trans.RWS.Lazy as LRWS
import qualified Control.Monad.Trans.RWS.Strict as SRWS
import qualified Control.Monad.Trans.State.Lazy as LS
import qualified Control.Monad.Trans.State.Strict as SS
import qualified Control.Monad.Trans.Writer.Lazy as LW
import qualified Control.Monad.Trans.Writer.Strict as SW
import           Data.ByteString (ByteString)
import           Data.Monoid
import           Data.Pool
import qualified Database.PostgreSQL.Simple as P

------------------------------------------------------------------------------
-- | The state for the postgresql-simple snaplet. To use it in your app
-- include this in your application state and use pgsInit to initialize it.
data Postgres = PostgresPool (Pool P.Connection)
              | PostgresConn P.Connection


------------------------------------------------------------------------------
-- | Instantiate this typeclass on 'Handler b YourAppState' so this snaplet
-- can find the connection source.  If you need to have multiple instances of
-- the postgres snaplet in your application, then don't provide this instance
-- and leverage the default instance by using \"@with dbLens@\" in front of calls
-- to snaplet-postgresql-simple functions.
class (MonadIO m, MonadBaseControl IO m) => HasPostgres m where
    getPostgresState :: m Postgres
    setLocalPostgresState :: Postgres -> m a -> m a


instance HasPostgres m => HasPostgres (IdentityT m) where
    getPostgresState = lift getPostgresState
    setLocalPostgresState pg (IdentityT m) = IdentityT $
      setLocalPostgresState pg m


instance HasPostgres m => HasPostgres (ListT m) where
    getPostgresState = lift getPostgresState
    setLocalPostgresState pg (ListT m) = ListT $
      setLocalPostgresState pg m


instance HasPostgres m => HasPostgres (MaybeT m) where
    getPostgresState = lift getPostgresState
    setLocalPostgresState pg (MaybeT m) = MaybeT $
      setLocalPostgresState pg m


instance HasPostgres m => HasPostgres (ReaderT r m) where
    getPostgresState = lift getPostgresState
    setLocalPostgresState pg (ReaderT m) = ReaderT $ \e ->
      setLocalPostgresState pg (m e)


instance (Monoid w, HasPostgres m) => HasPostgres (LW.WriterT w m) where
    getPostgresState = lift getPostgresState
    setLocalPostgresState pg (LW.WriterT m) = LW.WriterT $
      setLocalPostgresState pg m


instance (Monoid w, HasPostgres m) => HasPostgres (SW.WriterT w m) where
    getPostgresState = lift getPostgresState
    setLocalPostgresState pg (SW.WriterT m) = SW.WriterT $
      setLocalPostgresState pg m


instance HasPostgres m => HasPostgres (LS.StateT w m) where
    getPostgresState = lift getPostgresState
    setLocalPostgresState pg (LS.StateT m) = LS.StateT $ \s ->
      setLocalPostgresState pg (m s)


instance HasPostgres m => HasPostgres (SS.StateT w m) where
    getPostgresState = lift getPostgresState
    setLocalPostgresState pg (SS.StateT m) = SS.StateT $ \s ->
      setLocalPostgresState pg (m s)


instance (Monoid w, HasPostgres m) => HasPostgres (LRWS.RWST r w s m) where
    getPostgresState = lift getPostgresState
    setLocalPostgresState pg (LRWS.RWST m) = LRWS.RWST $ \e s ->
      setLocalPostgresState pg (m e s)


instance (Monoid w, HasPostgres m) => HasPostgres (SRWS.RWST r w s m) where
    getPostgresState = lift getPostgresState
    setLocalPostgresState pg (SRWS.RWST m) = SRWS.RWST $ \e s ->
      setLocalPostgresState pg (m e s)


------------------------------------------------------------------------------
-- | Data type holding all the snaplet's config information.
data PGSConfig = PGSConfig
    { pgsConnStr    :: ByteString
      -- ^ A libpq connection string.
    , pgsNumStripes :: Int
      -- ^ The number of distinct sub-pools to maintain. The smallest
      -- acceptable value is 1.
    , pgsIdleTime   :: Double
      -- ^ Amount of time for which an unused resource is kept open. The
      -- smallest acceptable value is 0.5 seconds.
    , pgsResources  :: Int
      -- ^ Maximum number of resources to keep open per stripe. The smallest
      -- acceptable value is 1.
    }


------------------------------------------------------------------------------
-- | Returns a config object with default values and the specified connection
-- string.
pgsDefaultConfig :: ByteString
                   -- ^ A connection string such as \"host=localhost
                   -- port=5432 dbname=mydb\"
                 -> PGSConfig
pgsDefaultConfig connstr = PGSConfig connstr 1 5 20



------------------------------------------------------------------------------
-- | Function that reserves a single connection for the duration of the given
-- action. Nested calls to withPG will only reserve one connection. For example,
-- the following code calls withPG twice in a nested way yet only results in a single
-- connection being reserved:
-- 
-- > myHandler = withPG $ do
-- >    queryTheDatabase
-- >    commonDatabaseMethod
-- >
-- > commonDatabaseMethod = withPG $ do
-- >    moreDatabaseActions
-- >    evenMoreDatabaseActions
--
-- This is useful in a practical setting because you may often find yourself in a situation
-- where you have common code (that requires a database connection) that you wish to call from 
-- other blocks of code that may require a database connection and you still want to make sure
-- that you are only using one connection through all of your nested methods.
withPG :: (HasPostgres m)
       => m b -> m b
withPG f = do
    s <- getPostgresState
    case s of
      (PostgresPool p) -> withResource p (\c -> setLocalPostgresState (PostgresConn c) f)
      (PostgresConn _) -> f


------------------------------------------------------------------------------
-- | Convenience function for executing a function that needs a database
-- connection.
liftPG :: (HasPostgres m) => (P.Connection -> m a) -> m a
liftPG act = do
   pg <- getPostgresState
   control $ \run ->
     withConnection pg $ \con -> run (act con)


-- | Convenience function for executing a function that needs a database
-- connection specialized to IO.
liftPG' :: (HasPostgres m) => (P.Connection -> IO b) -> m b
liftPG' f = do
    s <- getPostgresState
    withConnection s f


------------------------------------------------------------------------------
-- | Convenience function for executing a function that needs a database
-- connection.
withConnection :: MonadIO m => Postgres -> (P.Connection -> IO b) -> m b
withConnection (PostgresPool p) f = liftIO (withResource p f)
withConnection (PostgresConn c) f = liftIO (f c)
