{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE ForeignFunctionInterface #-}

-- | Store a stable pointer in a foreign context to be retrieved
-- later. Persists through GHCi reloads. Not thread-safe.

module Foreign.Store
  (-- * Foreign stores
   writeStore
  ,newStore
  ,lookupStore
  ,readStore
  ,deleteStore
  ,storeAction
  ,withStore
  ,Store(..)
  ,StoreException(..))
  where

import Control.Exception
import Data.Typeable
import Data.Word
import Foreign.Ptr
import Foreign.StablePtr

-- | An exception when working with stores.
data StoreException
  = StoreNotFound
  deriving (Show,Eq,Typeable)

instance Exception StoreException

-- | A hideously unsafe store. Only for use if you are suave.
newtype Store a =
  Store Word32
  deriving (Show,Eq)

-- | Lookup from the store if an index is allocated.
--
-- Not thread-safe.
lookupStore :: Word32 -> IO (Maybe (Store a))
lookupStore i =
  do r <- x_lookup i
     if r == 0
        then return Nothing
        else return (Just (Store i))

-- | Allocates or finds an unallocated store. The index is random. The
-- internal vector of stores grows in size. When stores are deleted
-- the vector does not shrink, but old slots are re-used.
--
-- Not thread-safe.
newStore :: a -> IO (Store a)
newStore a =
  do sptr <- newStablePtr a
     i <- x_store sptr
     return (Store i)

-- | Write to the store at the given index. If a store doesn't exist,
-- creates one and resizes the store vector to fit. If there is
-- already a store at the given index, deletes that store with
-- 'deleteStore' before replacing it.
--
-- Not thread-safe.
writeStore :: Store a -> a -> IO ()
writeStore (Store i) a =
  do existing <- lookupStore i
     maybe (return ()) deleteStore existing
     sptr <- newStablePtr a
     x_set i sptr
     return ()

-- | Read from the store. If the store has been deleted or is
-- unallocated, this will throw an exception.
--
-- Not thread-safe.
readStore :: Store a -> IO a
readStore (Store i) =
  do sptr <- x_get i
     if castStablePtrToPtr sptr == nullPtr
        then throw StoreNotFound
        else deRefStablePtr sptr

-- | Frees the stable pointer for GC and frees up the slot in the
-- store. Deleting an already deleted store is a no-op. But remember
-- that store numbers are re-used.
--
-- Not thread-safe.
deleteStore :: Store a -> IO ()
deleteStore (Store i) = do
  sptr <- x_get i
  if castStablePtrToPtr sptr == nullPtr
     then return ()
     else do freeStablePtr sptr
             x_delete i

-- | Run the action and store the result.
--
-- Not thread-safe.
storeAction :: Store a -> IO a -> IO a
storeAction s m =
  do v <- m
     writeStore s v
     return v

-- | Run the action with the value in the store.
--
-- Not thread-safe.
withStore :: Store a -> (a -> IO b) -> IO b
withStore s f =
  do v <- readStore s
     f v

foreign import ccall
  "x-helpers.h x_store"
  x_store :: StablePtr a -> IO Word32

foreign import ccall
  "x-helpers.h x_set"
  x_set :: Word32 -> StablePtr a -> IO ()

foreign import ccall
  "x-helpers.h x_get"
  x_get :: Word32 -> IO (StablePtr a)

foreign import ccall
  "x-helpers.h x_lookup"
  x_lookup :: Word32 -> IO Word32

foreign import ccall
  "x-helpers.h x_delete"
  x_delete :: Word32 -> IO ()