- | This is our dsl interpreter for these unit tests.  It's very similar
-- to 'Lib.runDbDSLInServant', except that it doesn't actually access
-- a database.  Instead, it just uses an 'IntMap' to simulate a database.

-- It's similar to 'runDbDSLInServant' in that if you curry the 'IORef'
-- argument, then you get a function @'DbDSL' a -> 'EitherT' 'ServantErr'
-- IO a@.  It takes a 'DbDSL' and evaluates it in a Servant context (e.g.
-- the @'EitherT' 'ServantErrr' IO@ monad).
--
-- It's using an 'IORef' to hold a tuple of the the 'IntMap' and 'Int'
-- corresponding to the id count for simplicity, but it could easily be
-- rewritten to use something like a 'State' monad.
--
-- The 'Int' corresponding to the id count is simply the highest id of
-- something in the database.  Everytime we insert something we increase
-- it by 1.
testDbDSLInServant :: IORef (IntMap BlogPost, Int)
                   -> DbDSL a
                   -> EitherT ServantErr IO a
testDbDSLInServant dbRef dbDSL = do
    case view dbDSL of
        Return a -> return a
        -- This evaluates a 'GetDb' request to actually get
        -- a 'BlogPost' from the hashmap.
        (GetDb key) :>>= nextStep -> do
            -- Get the 'IntMap' from the 'IORef'.
            (intMap, _) <- liftIO $ readIORef dbRef
            -- Lookup the key of the 'BlogPost' in the 'IntMap'.
            let maybeBlogPost = IntMap.lookup (sqlKeyToInt key) intMap
            -- Run the next step of the dsl, passing it the 'BlogPost'.
            testDbDSLInServant dbRef $ nextStep maybeBlogPost
        -- Evaluate a 'InsertDb' request to insert a 'BlogPost' in to the
        -- hashmap.
        (InsertDb blogPost) :>>= nextStep -> do
            (intMap, idCounter) <- liftIO $ readIORef dbRef
            let newIntMap = IntMap.insert idCounter blogPost intMap
                newCounter = idCounter + 1
            liftIO $ writeIORef dbRef (newIntMap, newCounter)
            testDbDSLInServant dbRef . nextStep $ intToSqlKey idCounter
        -- Evaluate a 'DelDb' request to delete a 'BlogPost' from the
        -- hashmap.
        (DelDb key) :>>= nextStep -> do
            (intMap, counter) <- liftIO $ readIORef dbRef
            let newIntMap = IntMap.delete (sqlKeyToInt key) intMap
            liftIO $ writeIORef dbRef (newIntMap, counter)
            testDbDSLInServant dbRef $ nextStep ()
        -- Evaluate an 'UpdateDb' request to update a 'BlogPost' in the
        -- hashmap.
        (UpdateDb key blogPost) :>>= nextStep -> do
            (intMap, counter) <- liftIO $ readIORef dbRef
            let newIntMap = IntMap.insert (sqlKeyToInt key) blogPost intMap
            when (sqlKeyToInt key `IntMap.member` intMap) $
                liftIO $ writeIORef dbRef (newIntMap, counter)
            testDbDSLInServant dbRef $ nextStep ()
        -- Throw an error to indicate that something went wrong.
        (ThrowDb servantErr) :>>= _ ->
            throwError servantErr
  where
    -- | Turn a 'Key' 'BlogPost' into an 'Int'.  This is for storing a 'Key'
    -- 'BlogPost' in our 'IntMap'.
    sqlKeyToInt :: Key BlogPost -> Int
    sqlKeyToInt key = fromInteger . toInteger $ fromSqlKey key

    -- | Opposite of 'sqlKeyToInt'.
    intToSqlKey :: Int -> Key BlogPost
    intToSqlKey int = toSqlKey . fromInteger $ toInteger int

-- | This creates a Wai 'Application'.
--
-- It just creates a new 'IORef' to our 'IntMap', and passes it to
-- 'testDbDSLInServant'.  It then uses the 'serve' function to create a Wai
-- 'Application'.

testDbDSLInServant :: IORef (IntMap BlogPost, Int)
                   -> DbDSL a
                   -> EitherT ServantErr IO a
testDbDSLInServant dbRef dbDSL = do
    case view dbDSL of
        Return a -> return a
        -- This evaluates a 'GetDb' request to actually get
        -- a 'BlogPost' from the hashmap.
        (GetDb key) :>>= nextStep -> do
            -- Get the 'IntMap' from the 'IORef'.
            (intMap, _) <- liftIO $ readIORef dbRef
            -- Lookup the key of the 'BlogPost' in the 'IntMap'.
            let maybeBlogPost = IntMap.lookup (sqlKeyToInt key) intMap
            -- Run the next step of the dsl, passing it the 'BlogPost'.
            testDbDSLInServant dbRef $ nextStep maybeBlogPost
        -- Evaluate a 'InsertDb' request to insert a 'BlogPost' in to the
        -- hashmap.
        (InsertDb blogPost) :>>= nextStep -> do
            (intMap, idCounter) <- liftIO $ readIORef dbRef
            let newIntMap = IntMap.insert idCounter blogPost intMap
                newCounter = idCounter + 1
            liftIO $ writeIORef dbRef (newIntMap, newCounter)
            testDbDSLInServant dbRef . nextStep $ intToSqlKey idCounter
        -- Evaluate a 'DelDb' request to delete a 'BlogPost' from the
        -- hashmap.
        (DelDb key) :>>= nextStep -> do
            (intMap, counter) <- liftIO $ readIORef dbRef
            let newIntMap = IntMap.delete (sqlKeyToInt key) intMap
            liftIO $ writeIORef dbRef (newIntMap, counter)
            testDbDSLInServant dbRef $ nextStep ()
        -- Evaluate an 'UpdateDb' request to update a 'BlogPost' in the
        -- hashmap.
        (UpdateDb key blogPost) :>>= nextStep -> do
            (intMap, counter) <- liftIO $ readIORef dbRef
            let newIntMap = IntMap.insert (sqlKeyToInt key) blogPost intMap
            when (sqlKeyToInt key `IntMap.member` intMap) $
                liftIO $ writeIORef dbRef (newIntMap, counter)
            testDbDSLInServant dbRef $ nextStep ()
        -- Throw an error to indicate that something went wrong.
        (ThrowDb servantErr) :>>= _ ->
            throwError servantErr
  where
    -- | Turn a 'Key' 'BlogPost' into an 'Int'.  This is for storing a 'Key'
    -- 'BlogPost' in our 'IntMap'.
    sqlKeyToInt :: Key BlogPost -> Int
    sqlKeyToInt key = fromInteger . toInteger $ fromSqlKey key

    -- | Opposite of 'sqlKeyToInt'.
    intToSqlKey :: Int -> Key BlogPost
    intToSqlKey int = toSqlKey . fromInteger $ toInteger int

-- | This creates a Wai 'Application'.
--
-- It just creates a new 'IORef' to our 'IntMap', and passes it to
-- 'testDbDSLInServant'.  It then uses the 'serve' function to create a Wai
-- 'Application'.
testDbDSLInServant dbRef dbDSL = do
    case view dbDSL of
        Return a -> return a
        -- This evaluates a 'GetDb' request to actually get
        -- a 'BlogPost' from the hashmap.
        (GetDb key) :>>= nextStep -> do
            -- Get the 'IntMap' from the 'IORef'.
            (intMap, _) <- liftIO $ readIORef dbRef
            -- Lookup the key of the 'BlogPost' in the 'IntMap'.
            let maybeBlogPost = IntMap.lookup (sqlKeyToInt key) intMap
            -- Run the next step of the dsl, passing it the 'BlogPost'.
            testDbDSLInServant dbRef $ nextStep maybeBlogPost
        -- Evaluate a 'InsertDb' request to insert a 'BlogPost' in to the
        -- hashmap.
        (InsertDb blogPost) :>>= nextStep -> do
            (intMap, idCounter) <- liftIO $ readIORef dbRef
            let newIntMap = IntMap.insert idCounter blogPost intMap
                newCounter = idCounter + 1
            liftIO $ writeIORef dbRef (newIntMap, newCounter)
            testDbDSLInServant dbRef . nextStep $ intToSqlKey idCounter
        -- Evaluate a 'DelDb' request to delete a 'BlogPost' from the
        -- hashmap.
        (DelDb key) :>>= nextStep -> do
            (intMap, counter) <- liftIO $ readIORef dbRef
            let newIntMap = IntMap.delete (sqlKeyToInt key) intMap
            liftIO $ writeIORef dbRef (newIntMap, counter)
            testDbDSLInServant dbRef $ nextStep ()
        -- Evaluate an 'UpdateDb' request to update a 'BlogPost' in the
        -- hashmap.
        (UpdateDb key blogPost) :>>= nextStep -> do
            (intMap, counter) <- liftIO $ readIORef dbRef
            let newIntMap = IntMap.insert (sqlKeyToInt key) blogPost intMap
            when (sqlKeyToInt key `IntMap.member` intMap) $
                liftIO $ writeIORef dbRef (newIntMap, counter)
            testDbDSLInServant dbRef $ nextStep ()
        -- Throw an error to indicate that something went wrong.
        (ThrowDb servantErr) :>>= _ ->
            throwError servantErr
  where
    -- | Turn a 'Key' 'BlogPost' into an 'Int'.  This is for storing a 'Key'
    -- 'BlogPost' in our 'IntMap'.
    sqlKeyToInt :: Key BlogPost -> Int
    sqlKeyToInt key = fromInteger . toInteger $ fromSqlKey key

    -- | Opposite of 'sqlKeyToInt'.
    intToSqlKey :: Int -> Key BlogPost
    intToSqlKey int = toSqlKey . fromInteger $ toInteger int

-- | This creates a Wai 'Application'.
--
-- It just creates a new 'IORef' to our 'IntMap', and passes it to
-- 'testDbDSLInServant'.  It then uses the 'serve' function to create a Wai
-- 'Application'.