{-# LANGUAGE LambdaCase     #-}
{-# LANGUAGE NamedFieldPuns #-}
{-|
Module: Internal.Rc
Description: Reference counted boxes.

This module provides a reference-counted cell type 'Rc', which contains a
value and a finalizer. When the reference count reaches zero, the value is
dropped and the finalizer is run.
-}
module Internal.Rc
    ( Rc
    , new
    , get
    , incr
    , decr
    , release
    ) where

import Control.Concurrent.STM

-- | A reference-counted container for a value of type @a@.
newtype Rc a
    = Rc (TVar (Maybe (RcState a)))
    deriving(Eq)

data RcState a = RcState
    { refCount  :: !Int
    , value     :: a
    , finalizer :: STM ()
    }

-- | @'new' val finalizer@ creates a new 'Rc' containing the value @val@, with
-- an initial reference count of 1. When the reference count drops to zero, the
-- finalizer will be run.
new :: a -> STM () -> STM (Rc a)
new value finalizer = fmap Rc $ newTVar $ Just RcState
    { refCount = 1
    , value
    , finalizer
    }

-- | Increment the reference count.
incr :: Rc a -> STM ()
incr (Rc tv) = modifyTVar' tv $
    fmap $ \s@RcState{refCount} -> s { refCount = refCount + 1 }

-- | Decrement the reference count. If this brings the count to zero, run the
-- finalizer and release the value.
decr :: Rc a -> STM ()
decr (Rc tv) = readTVar tv >>= \case
    Nothing ->
        pure ()
    Just RcState{refCount=1, finalizer} -> do
        writeTVar tv Nothing
        finalizer
    Just s@RcState{refCount} ->
        writeTVar tv $ Just s { refCount = refCount - 1 }

-- | Release the value immediately, and run the finalizer, regardless of the
-- current reference count.
release :: Rc a -> STM ()
release (Rc tv) = readTVar tv >>= \case
    Nothing ->
        pure ()
    Just RcState{finalizer} -> do
        finalizer
        writeTVar tv Nothing

-- | Fetch the value, or 'Nothing' if it has been released.
get :: Rc a -> STM (Maybe a)
get (Rc tv) = fmap value <$> readTVar tv