{-# OPTIONS_GHC -Wall -fwarn-tabs #-}
{-# LANGUAGE CPP, DeriveDataTypeable #-}
----------------------------------------------------------------
--                                                    2011.04.05
-- |
-- Module      :  Control.Concurrent.STM.TBChan
-- Copyright   :  Copyright (c) 2011 wren ng thornton
-- License     :  BSD
-- Maintainer  :  wren@community.haskell.org
-- Stability   :  experimental
-- Portability :  non-portable (GHC STM, DeriveDataTypeable)
--
-- A version of "Control.Concurrent.STM.TChan" where the queue is
-- bounded in length.
----------------------------------------------------------------
module Control.Concurrent.STM.TBChan
    (
    -- * The TBChan type
      TBChan()
    -- ** Creating TBChans
    , newTBChan
    , newTBChanIO
    -- I don't know how to define dupTBChan with the correct semantics
    -- ** Reading from TBChans
    , readTBChan
    , tryReadTBChan
    , peekTBChan
    , tryPeekTBChan
    -- ** Writing to TBChans
    , writeTBChan
    , tryWriteTBChan
    , unGetTBChan
    -- ** Predicates
    , isEmptyTBChan
    , isFullTBChan
    ) where

import Data.Typeable     (Typeable)
import Control.Monad.STM (STM, retry)
import Control.Concurrent.STM.TVar.Compat
import Control.Concurrent.STM.TChan.Compat -- N.B., GHC only

-- N.B., we need a Custom cabal build-type for this to work.
#ifdef __HADDOCK__
import Control.Monad.STM (atomically)
import System.IO.Unsafe  (unsafePerformIO)
#endif
----------------------------------------------------------------

-- | @TBChan@ is an abstract type representing a bounded FIFO
-- channel.
data TBChan a = TBChan !(TVar Int) !(TChan a)
    deriving (Typeable)


-- | Build and returns a new instance of @TBChan@ with the given
-- capacity. /N.B./, we do not verify the capacity is positive, but
-- if it is non-positive then 'writeTBChan' will always retry and
-- 'isFullTBChan' will always be true.
newTBChan :: Int -> STM (TBChan a)
newTBChan n = do
    limit <- newTVar n
    chan  <- newTChan
    return (TBChan limit chan)


-- | @IO@ version of 'newTBChan'. This is useful for creating
-- top-level @TBChan@s using 'unsafePerformIO', because using
-- 'atomically' inside 'unsafePerformIO' isn't possible.
newTBChanIO :: Int -> IO (TBChan a)
newTBChanIO n = do
    limit <- newTVarIO n
    chan  <- newTChanIO
    return (TBChan limit chan)


-- | Read the next value from the @TBChan@, retrying if the channel
-- is empty.
readTBChan :: TBChan a -> STM a
readTBChan (TBChan limit chan) = do
    x <- readTChan chan
    modifyTVar' limit (1 +)
    return x


-- | A version of 'readTBChan' which does not retry. Instead it
-- returns @Nothing@ if no value is available.
tryReadTBChan :: TBChan a -> STM (Maybe a)
tryReadTBChan (TBChan limit chan) = do
    mx <- tryReadTChan chan
    case mx of
        Nothing -> return Nothing
        Just _x -> do
            modifyTVar' limit (1 +)
            return mx


-- | Get the next value from the @TBChan@ without removing it,
-- retrying if the channel is empty.
peekTBChan :: TBChan a -> STM a
peekTBChan (TBChan _limit chan) =
    peekTChan chan


-- | A version of 'peekTBChan' which does not retry. Instead it
-- returns @Nothing@ if no value is available.
tryPeekTBChan :: TBChan a -> STM (Maybe a)
tryPeekTBChan (TBChan _limit chan) =
    tryPeekTChan chan


-- | Write a value to a @TBChan@, retrying if the channel is full.
writeTBChan :: TBChan a -> a -> STM ()
writeTBChan self@(TBChan limit chan) x = do
    b <- isFullTBChan self
    if b
        then retry
        else do
            writeTChan chan x
            modifyTVar' limit (subtract 1)


-- | A version of 'writeTBChan' which does not retry. Returns @True@
-- if the value was successfully written, and @False@ otherwise.
tryWriteTBChan :: TBChan a -> a -> STM Bool
tryWriteTBChan self@(TBChan limit chan) x = do
    b <- isFullTBChan self
    if b
        then return False
        else do
            writeTChan chan x
            modifyTVar' limit (subtract 1)
            return True


-- | Put a data item back onto a channel, where it will be the next
-- item read. /N.B./, this could allow the channel to temporarily
-- become longer than the specified limit, which is necessary to
-- ensure that the item is indeed the next one read.
unGetTBChan :: TBChan a -> a -> STM ()
unGetTBChan (TBChan limit chan) x = do
    unGetTChan chan x
    modifyTVar' limit (subtract 1)


-- | Returns @True@ if the supplied @TBChan@ is empty (i.e., has
-- no elements). /N.B./, a @TBChan@ can be both ``empty'' and
-- ``full'' at the same time, if the initial limit was non-positive.
isEmptyTBChan :: TBChan a -> STM Bool
isEmptyTBChan (TBChan _limit chan) =
    isEmptyTChan chan


-- | Returns @True@ if the supplied @TBChan@ is full (i.e., is over
-- its limit). /N.B./, a @TBChan@ can be both ``empty'' and ``full''
-- at the same time, if the initial limit was non-positive.
isFullTBChan :: TBChan a -> STM Bool
isFullTBChan (TBChan limit _chan) = do
    n <- readTVar limit
    return $! n <= 0

----------------------------------------------------------------
----------------------------------------------------------- fin.