{-# LANGUAGE NamedFieldPuns #-}
module Control.Concurrent.BQueue
  ( BQueue
  , newBQueue
  , pushBQueue
  , popBQueue
  , takeBQueue
  , flushBQueue
  , lengthBQueue
  ) where

-- | FIFO Bounded Queue with O(1) amortized popping.
data BQueue a = BQueue
  { bqRead      :: ![a]
  , bqReadSize  :: {-# UNPACK #-}!Int
  , bqWrite     :: ![a]
  , bqWriteSize :: {-# UNPACK #-}!Int
  , bqMaxSize   :: {-# UNPACK #-}!Int
  }

-- | Create new Bounded Queue
newBQueue :: Int -- ^ Upper bound on the numer of elements the queue can hold.
          -> BQueue a
newBQueue bqMaxSize =
  BQueue {bqRead = [], bqReadSize = 0, bqWrite = [], bqWriteSize = 0, bqMaxSize}

-- | Push an element onto the queue. Returns the new queue with the element placed onto the right
-- side of the source queue, but only if the maximum bound hasn't been reached, otherwise it will
-- return `Nothing`
pushBQueue :: a -> BQueue a -> Maybe (BQueue a)
pushBQueue x bq@BQueue {bqWrite, bqWriteSize}
  | bqReadSize bq + bqWriteSize < bqMaxSize bq =
    Just bq {bqWrite = x : bqWrite, bqWriteSize = bqWriteSize + 1}
  | otherwise = Nothing


-- | Pop an element from the queue. Returns the leftmost element from the queue together with new
-- queue, lacking that element, `Nothing` if the queue was empty.
popBQueue :: BQueue a -> Maybe (a, BQueue a)
popBQueue bq@BQueue {bqRead, bqReadSize, bqWrite, bqWriteSize} =
  case bqRead of
    (x:xs) -> Just (x, bq {bqRead = xs, bqReadSize = bqReadSize - 1})
    [] ->
      case reverse bqWrite of
        (y:ys) ->
          Just (y, bq {bqRead = ys, bqReadSize = bqWriteSize - 1, bqWrite = [], bqWriteSize = 0})
        [] -> Nothing

-- | /O(n) - Get all the elements from the Bounded Queue.
flushBQueue :: BQueue a -> ([a], BQueue a)
flushBQueue bq = (bqRead bq ++ reverse (bqWrite bq), newBQueue (bqMaxSize bq))

-- | /O(i)/ - Take @i@ elements from the Bounded Queue. This function doesn't fail - it returns empty
-- list on negative @i@ and all elements there is if requested more than available.
takeBQueue :: Int -> BQueue a -> ([a], BQueue a)
takeBQueue i bq@BQueue {bqRead, bqReadSize, bqWrite, bqWriteSize}
  | i < bqReadSize =
    let (taken, leftover) = splitAt i bqRead
     in (taken, bq {bqRead = leftover, bqReadSize = bqReadSize - max 0 i})
  | i < totalSize =
    let (taken, leftover) = splitAt i (bqRead ++ reverse bqWrite)
     in (taken, bq {bqRead = leftover, bqReadSize = totalSize - i, bqWrite = [], bqWriteSize = 0})
  | otherwise = flushBQueue bq
  where
    totalSize = bqReadSize + bqWriteSize


-- | /O(1)/ - Get the current length of a queue
lengthBQueue :: BQueue a -> Int
lengthBQueue bq = bqReadSize bq + bqWriteSize bq