{-# LANGUAGE OverloadedStrings #-} 

-- | Solution for caching mandatory data with Redis.
--   
--   In many cases, requires not just pick up or put the data into the cache.
--   As a rule, data are required. 
--   
--   ... check the cache ... if the value is missing, run the calculations ... 
--   put value to cache ... Tedious
--   
--   Solution is quite simple - collapse all of these steps in one operation.
module Database.Redis.Pile (
    pile
) where

import qualified Data.ByteString as B

import Control.Monad.IO.Class (liftIO)
import Control.Monad (void)

import qualified Database.Redis as R
import qualified Database.Redis.Tags as RT

-- | Stores computation results in Redis as hashSet. Computation fires only  
--   if data absent in cache. Of course, to refresh the data, they must first 
--   remove from the cache.
--   
--   Computation controls all that will be stored in the cache except two 
--   things: key and prefix for keys and tags. To do this, 
--   with the results of computation, it may return optional @TTL@ in 
--   seconds (Redis convention) and tags for key. About tags see 
--   "Database.Redis.Tags".
--   
--   Instead get all data from cache, optional parameter allows simply make 
--   sure that cache holds HashSet with needed field with needed value. If this 
--   is so, 'pile' return 'Nothing'. 
--
-- > conn <- connect defaultConnectInfo
-- > runRedis conn $ do
-- >    -- do it
-- >    r <- pile "myprefix" "mykey" (Just ("etag", "etag")) Nothing $  
-- >        return ([("etag", "etag"), ("val", "myval")], Nothing, [])
-- >    liftIO $ print r
-- >    -- Just [("etag", "etag"), ("val", "myval")]
-- >    
-- >    -- once again
-- >    r <- pile "myprefix" "mykey" (Just ("etag", "etag")) Nothing $  
-- >        return ([("etag", "etag"), ("val", "myval")], Nothing, [])
-- >    liftIO $ print r
-- >    -- Nothing
-- >    
-- >    -- and again without expect
-- >    r <- pile "myprefix" "mykey" Nothing Nothing $  
-- >        return ([("etag", "etag"), ("val", "myval")], Nothing, [])
-- >    liftIO $ print r
-- >    -- Just [("etag", "etag"), ("val", "myval")]
--
--   Time complexity for cached data:
--   
--   * @O(1)@ If expect matches.
--
--   * @O(2)@ If payload field provided
pile :: 
       B.ByteString
            -- ^ Prefix for key and tags.
    -> B.ByteString        
            -- ^ Key in cache. Key will be stored as @prefix:key@
    -> Maybe (B.ByteString, B.ByteString)
            -- ^ Optional expect field.
    -> Maybe B.ByteString
            -- ^ Optional payload field. This reduces time complexity of 
            --   request data from the cache from @O(N)@ to @O(1)@.
            --   Regardless of setting this field, all data from computation
            --   will stored in cache.
    -> IO ([(B.ByteString, B.ByteString)], 
           Maybe Integer, 
           [B.ByteString])
            -- ^ Computation that returns data and 
            --   optional TTL and tags. All tags will be stored as @prefix:tag@.
    -> R.Redis (Maybe [(B.ByteString, B.ByteString)])
pile p key (Just (ef, ev)) payload f = do
    e <- R.hget (p `B.append` ":" `B.append` key) ef
    case e of
        Right (Just ev') | ev' == ev -> return Nothing
                         | otherwise -> pile p key Nothing payload f
        _ -> pile p key Nothing payload f
pile p key Nothing payload f = do
    d <- fetchPayload payload
    case d of
        Nothing -> runF
        Just r -> return $ Just r
  where
    fetchPayload Nothing = do
        v <- R.hgetall withPrefix
        return $ case v of 
            Right [] -> Nothing
            Right r -> Just r
            _ -> Nothing
    fetchPayload (Just plf) = do
        v <- R.hget withPrefix plf
        return $ case v of 
            Right (Just v') -> Just [(plf, v')]
            _ -> Nothing
    
    withPrefix = p `B.append` ":" `B.append` key
    runF = do
        (r, ke, t) <- liftIO f
        void $ R.hmset withPrefix r
        setExpire ke
        RT.markTags [withPrefix] p t
        return $ Just r
      where
        setExpire Nothing = return ()
        setExpire (Just ke) = void $ R.expire withPrefix ke