{-# LANGUAGE BangPatterns #-}

module Database.VCache.VPutFini
    ( vputFini
    , runVPutIO
    , runVPut
    ) where

import Control.Exception (onException)
import Data.IORef
import Data.ByteString (ByteString)
import Foreign.Ptr
import Foreign.ForeignPtr
import Foreign.Marshal.Alloc
import System.IO.Unsafe (unsafePerformIO)
import qualified Data.ByteString.Internal as BSI

import Database.VCache.Types
import Database.VCache.VPutAux

-- | When we're just about done with VPut, we really have one more
-- task to perform: to output the address list for any contained 
-- PVars and VRefs. These addresses are simply concatenated onto the
-- normal byte output, with a final size value (not including itself)
-- to indicate how far to jump back.
--
-- Actually, we output the first address followed by relative offsets
-- for every following address. This behavior allows us to reduce the
-- serialization costs when addresses are near each other in memory.
--
-- The address list is output in the reverse order of serialization.
-- (This simplifies reading in the same order as serialization without
-- a list reversal operation.)
--
-- It's important that we finalize exactly once for every serialization,
-- and that this be applied before any hash functions.
vputFini :: VPut ()
vputFini = do
    szStart <- peekBufferSize
    lChildren <- peekChildren
    putChildren lChildren
    szFini <- peekBufferSize
    putVarNatR (szFini - szStart)
    -- shrinkBuffer

putChildren :: [PutChild] -> VPut ()
putChildren [] = return ()
putChildren (x:xs) = 
    let addr0 = putChildAddr x in
    putVarNat (fromIntegral addr0) >>
    putChildren' addr0 xs

-- putChildren after the first, using offsets.
putChildren' :: Address -> [PutChild] -> VPut ()
putChildren' _ [] = return ()
putChildren' !prev (x:xs) = 
    let addrX = putChildAddr x in
    let offset = (fromIntegral addrX) - (fromIntegral prev) in
    putVarInt offset >>
    putChildren' addrX xs

runVPutIO :: VSpace -> VPut a -> IO (a, ByteString, [PutChild])
runVPutIO vs action = do
    let initialSize = 1000 -- avoid reallocs for small data
    pBuff <- mallocBytes initialSize
    vBuff <- newIORef pBuff
    let s0 = VPutS { vput_space = vs
                   , vput_children = []
                   , vput_buffer = vBuff
                   , vput_target = pBuff
                   , vput_limit = pBuff `plusPtr` initialSize
                   }
    let freeBuff = readIORef vBuff >>= free
    let fullWrite = do { result <- action; vputFini; return result }
    let runPut = _vput fullWrite s0
    (VPutR r sf) <- runPut `onException` freeBuff
    pBuff' <- readIORef vBuff
    let len = vput_target sf `minusPtr` pBuff'
    pBuffR <- reallocBytes pBuff len -- reclaim unused space
    fpBuff' <- newForeignPtr finalizerFree pBuffR
    let bytes = BSI.fromForeignPtr fpBuff' 0 len
    return (r, bytes, vput_children sf)
{-# NOINLINE runVPutIO #-}

runVPut :: VSpace -> VPut a -> (a, ByteString, [PutChild])
runVPut vs action = unsafePerformIO (runVPutIO vs action)