-----------------------------------------------------------------------------
--
-- Module      :  Memoization
-- Copyright   :  Alberto GOmez Corona
-- License     :  BSD3
--
-- Maintainer  :  agocorona@gmail.com
-- Stability   :  Experimental
-- Portability :  Non portable (uses stablenames)
--
-- |
--
-----------------------------------------------------------------------------
{-# OPTIONS_GHC -fno-warn-orphans -fno-warn-missing-signatures #-}
{-# LANGUAGE  DeriveDataTypeable
            , ExistentialQuantification
            , FlexibleInstances
            , TypeSynonymInstances  #-}
module Data.TCache.Memoization (writeCached,cachedByKey,cachedByKeySTM,flushCached,cachedp,addrStr,Executable(..))

where
import Data.Typeable
import Data.TCache
import Data.TCache.Defs(Indexable(..))
import System.IO.Unsafe
import System.Time
import Data.Maybe(fromJust)
import Control.Monad.Trans
import Control.Monad.Identity
import Data.RefSerialize(addrHash,newContext)
--import Debug.Trace
--(!>)= flip trace

data Cached a b= forall m.Executable m => Cached a (a -> m b) b Integer deriving Typeable

{-# NOINLINE context #-}
context = unsafePerformIO newContext

-- | given a string, return a key that can be used in Indexable instances
--  Of non persistent objects, such are cached objects (it changes fron execution to execution)
-- . It uses `addrHash`
addrStr :: a -> String
addrStr x= "addr" ++ show hash
 where
 hash = case unsafePerformIO $ addrHash context x of
   Right x1 -> x1
   Left x1  -> x1

-- | to execute a monad for the purpose of memoizing its result
class Executable m where
  execute:: m a -> a

instance Executable IO where
  execute m = unsafePerformIO $! f1 m ""
   where
   f1 m1 _= m1

instance Executable Identity where
  execute (Identity x)= x

instance MonadIO Identity where
  liftIO f=  Identity $!  unsafePerformIO $! f


cachedKeyPrefix :: String
cachedKeyPrefix = "cached"

instance  (Indexable a) => IResource (Cached a  b) where
  keyResource (Cached a  _ _ _)= cachedKeyPrefix ++ key a   -- ++ unsafePerformIO (addrStr f )

  writeResource _= return ()
  delResource _= return ()
  readResourceByKey _= return Nothing -- error $ "access By key is undefined for cached objects.key= " ++ k


  readResource (Cached a f _ _)=do
   TOD tnow _ <- getClockTime
   let b = execute $ f a
   return . Just $ Cached a f b tnow  -- !> "readRe"

--cache time f a=  do
--   TOD tnow _ <- getClockTime
--   let b = execute $ f a
--   withResources [] . const $ [Cached a f b tnow]     -- !> "writeRe"]
--
--cacheKey key time f= cache time (const  f) key

-- | memoize the result of a computation for a certain time. This is useful for  caching  costly data
-- such  web pages composed on the fly.
--
-- time == 0 means infinite

--getCachedRef :: (Indexable a,Typeable a, Typeable b) => a -> DBRef (Cached a b)
--getCachedRef x = getDBRef $ keyResource (Cached x (u u u) where u= undefined

writeCached
  :: (Typeable b, Typeable a, Indexable a, Executable m) =>
     a -> (a -> m b) -> b -> Integer -> STM ()
writeCached  a b c d=
    withSTMResources [] . const $ resources{toAdd= [Cached a b c d] }


cached ::  (Indexable a,Typeable a,  Typeable b, Executable m,MonadIO m) => Int -> (a -> m b) -> a  -> m b
cached time  f a= liftIO . atomically $ cachedSTM time f a

cachedSTM :: (Typeable a, Typeable b, Executable m, Indexable a, Integral p) => p -> (a -> m b) -> a -> STM b
cachedSTM time f a= do
   let prot= Cached a f undefined undefined
   let ref= getDBRef $ keyResource prot
   (Cached _ _ b t) <- readDBRef ref `onNothing` fillIt ref prot
   case time of
     0 -> return b
     _ -> do
           TOD tnow _ <- unsafeIOToSTM getClockTime
           if tnow - t >= fromIntegral time
                      then do
                            Cached _ _ b1 _ <- fillIt ref prot
                            return b1
                      else  return b
   where
   -- has been invalidated by flushCached
   fillIt ref proto= do
     let r = unsafePerformIO $return . fromJust =<< readResource proto   -- !> "fillIt"
     writeDBRef ref r
     return r

-- | Memoize the result of a computation for a certain time. A string 'key' is used to index the result
--
-- The Int parameter is the timeout, in second after the last evaluation, after which the cached value will be discarded and the expression will be evaluated again if demanded
-- . Time == 0 means no timeout
cachedByKey :: (Typeable a, Executable m,MonadIO m) => String -> Int ->  m a -> m a
cachedByKey key1 time  f = cached time (const f) key1

cachedByKeySTM :: (Typeable a, Executable m) => String -> Int ->  m a -> STM a
cachedByKeySTM key1 time  f = cachedSTM  time (const f) key1

-- Flush the cached object indexed by the key
flushCached :: String -> IO ()
flushCached k= atomically $ invalidateKey $ cachedKeyPrefix ++ k           -- !> "flushCached"

-- | a pure version of cached
cachedp :: (Indexable a,Typeable a,Typeable b) => (a ->b) -> a -> b
cachedp f k = execute $ cached 0 (Identity . f) k

--testmemo= do
--   let f x = "hi"++x  !> "exec1"
--   let f1 x= "h0"++x  !> "exec2"
--   let beacon=1
--   let beacon2=2
--   print $ cachedp f (addrStr "sfs")
--   print $ cachedp f (addrStr "sds")
--   print $ cachedp f1 (addrStr "ssdfddd")
--   print $ cachedp f1 (addrStr "sss")


