{-# LANGUAGE BangPatterns #-}

module Database.VCache.VRef
    ( VRef
    , vref, deref
    , vref', deref'
    , unsafeVRefAddr
    , unsafeVRefRefct
    , vref_space
    , CacheMode(..)
    , vrefc, derefc

    , withVRefBytes
    , unsafeVRefEncoding

    ) where

import Control.Monad
import Data.IORef
import Data.Bits
import Data.Word
import Data.ByteString (ByteString)
import Foreign.Ptr
import Foreign.Storable
import System.IO.Unsafe

import Database.VCache.Types
import Database.VCache.Alloc
import Database.VCache.Read

-- | Construct a reference with the cache initially active, i.e.
-- such that immediate deref can access the value without reading
-- from the database. The given value will be placed in the cache
-- unless the same vref has already been constructed.
vref :: (VCacheable a) => VSpace -> a -> VRef a
vref = vrefc CacheMode1
{-# INLINE vref #-}

-- | Construct a VRef with an alternative cache control mode.
vrefc :: (VCacheable a) => CacheMode -> VSpace -> a -> VRef a
vrefc cm vc v = unsafePerformIO (newVRefIO vc v cm)
{-# INLINABLE vrefc #-}

-- | In some cases, developers can reasonably assume they won't need a
-- value in the near future. In these cases, use the vref' constructor
-- to allocate a VRef without caching the content.
vref' :: (VCacheable a) => VSpace -> a -> VRef a
vref' vc v = unsafePerformIO (newVRefIO' vc v)
{-# INLINABLE vref' #-}

readVRef :: VRef a -> IO (a, Int)
readVRef v = readAddrIO (vref_space v) (vref_addr v) (vref_parse v)
{-# INLINE readVRef #-}

-- | Dereference a VRef, obtaining its value. If the value is not in
-- cache, it will be read into the database then cached. Otherwise,
-- the value is read from cache and the cache is touched to restart
-- any expiration.
--
-- Assuming a valid VCacheable instance, this operation should return
-- an equivalent value as was used to construct the VRef.
deref :: VRef a -> a
deref = derefc CacheMode1
{-# INLINE deref #-}

-- | Dereference a VRef with an alternative cache control mode.
derefc :: CacheMode -> VRef a -> a
derefc cm v = unsafeDupablePerformIO $
    unsafeInterleaveIO (readVRef v) >>= \ lazy_read_rw ->
    join $ atomicModifyIORef (vref_cache v) $ \case
        Cached r bf ->
            let bf' = touchCache cm bf in
            let c' = Cached r bf' in
            (c', c' `seq` return r)
        NotCached ->
            let (r,w) = lazy_read_rw in
            let c' = mkVRefCache r w cm in
            let op = initVRefCache v >> return r in
            (c', c' `seq` op)
{-# NOINLINE derefc #-}

-- | Dereference a VRef. This will read from the cache if the value
-- is available, but will not update the cache. If the value is not
-- cached, it will be read instead from the persistence layer.
--
-- This can be useful if you know you'll only dereference a value
-- once for a given task, or if the datatype involved is cheap to
-- parse (e.g. simple bytestrings) such that there isn't a strong
-- need to cache the parse result.
deref' :: VRef a -> a
deref' v = unsafePerformIO $
    readIORef (vref_cache v) >>= \case
        Cached r _ -> return r
        NotCached -> fmap fst (readVRef v)
{-# INLINABLE deref' #-}

-- | Specialized, zero-copy access to a `VRef ByteString`. Access to
-- the given ByteString becomes invalid after returning. This operation
-- may also block the writer if it runs much longer than a single
-- writer batch (though, writer batches are frequently large enough
-- that this shouldn't be a problem if you're careful).
--
withVRefBytes :: VRef ByteString -> (Ptr Word8 -> Int -> IO a) -> IO a
withVRefBytes v action = unsafeVRefEncoding v $ \ p n ->
    -- valid ByteString encoding: varNat, bytes, 0 (children)
    readVarNat p 0 >>= \ (p', n') ->
    let bOK = (p' `plusPtr` n') == (p `plusPtr` (n-1)) in
    let eMsg = show v ++ " doesn't contain a ByteString" in
    unless bOK (fail $ "withVRefBytes: " ++ eMsg) >>
    action p' n'

readVarNat :: Ptr Word8 -> Int -> IO (Ptr Word8, Int)
readVarNat !p !n =
    peek p >>= \ w8 ->
    let p' = p `plusPtr` 1 in
    let n' = (n `shiftL` 7) + fromIntegral (w8 .&. 0x7f) in
    let bDone = (0 == (w8 .&. 0x80)) in
    if bDone then return (p', n') else
    readVarNat p' n'

-- | Zero-copy access to the raw encoding for any VRef. The given data
-- becomes invalid after returning. This is provided for mostly for
-- debugging purposes, i.e. so you can peek under the hood and see how
-- things are encoded or eyeball the encoding.
unsafeVRefEncoding :: VRef any -> (Ptr Word8 -> Int -> IO a) -> IO a
unsafeVRefEncoding v = withBytesIO (vref_space v) (vref_addr v)
{-# INLINE unsafeVRefEncoding #-}

-- | Each VRef has an numeric address in the VSpace. This address is
-- non-deterministic, and essentially independent of the arguments to
-- the vref constructor. This function is 'unsafe' in the sense that
-- it violates the illusion of purity. However, the VRef address will
-- be stable so long as the developer can guarantee it is reachable.
--
-- This function may be useful for memoization tables and similar.
--
-- The 'Show' instance for VRef will also show the address.
unsafeVRefAddr :: VRef a -> Address
unsafeVRefAddr = vref_addr
{-# INLINE unsafeVRefAddr #-}

-- | This function allows developers to access the reference count
-- for the VRef that is currently recorded in the database. This may
-- be useful for heuristic purposes. However, caveats are needed:
--
-- First, due to structure sharing, a VRef may share an address with
-- VRefs of other types having the same serialized form. Reference
-- counts are at the address level.
--
-- Second, because the VCache writer operates in a background thread,
-- the reference count returned here may be slightly out of date.
--
-- Third, it is possible that VCache will eventually use some other
-- form of garbage collection than reference counting. This function
-- should be considered an unstable element of the API.
unsafeVRefRefct :: VRef a -> IO Int
unsafeVRefRefct v = readRefctIO (vref_space v) (vref_addr v)
{-# INLINE unsafeVRefRefct #-}



