{-# LANGUAGE BangPatterns      #-}
{-# LANGUAGE CPP               #-}
{-# LANGUAGE OverloadedStrings #-}

-- | Buffering for output streams based on bytestring builders.
--
-- Buffering an output stream can often improve throughput by reducing the
-- number of system calls made through the file descriptor. The @bytestring@
-- package provides an efficient monoidal datatype used for serializing values
-- directly to an output buffer, called a 'Builder', originally implemented in
-- the @blaze-builder@ package by Simon Meier. When compiling with @bytestring@
-- versions older than 0.10.4, (i.e. GHC <= 7.6) users must depend on the
-- @bytestring-builder@ library to get the new builder implementation. Since we
-- try to maintain compatibility with the last three GHC versions, the
-- dependency on @bytestring-builder@ can be dropped after the release of GHC
-- 7.12.
--
--
-- /Using this module/
--
-- Given an 'OutputStream' taking 'ByteString':
--
-- > someOutputStream :: OutputStream ByteString
--
-- You create a new output stream wrapping the original one that accepts
-- 'Builder' values:
--
--
-- @
-- do
--     newStream <- Streams.'builderStream' someOutputStream
--     Streams.'write' ('Just' $ 'Data.ByteString.Builder.byteString' \"hello\") newStream
--     ....
-- @
--
--
-- You can flush the output buffer using 'Data.ByteString.Builder.Extra.flush':
--
-- @
--     ....
--     Streams.'write' ('Just' 'Data.ByteString.Builder.Extra.flush') newStream
--     ....
-- @
--
-- As a convention, 'builderStream' will write the empty string to the wrapped
-- 'OutputStream' upon a builder buffer flush. Output streams which receive
-- 'ByteString' should either ignore the empty string or interpret it as a
-- signal to flush their own buffers, as the @handleToOutputStream@ and
-- "System.IO.Streams.Zlib" functions do.
--
-- /Example/
--
-- @
-- example :: IO [ByteString]
-- example = do
--     let l1 = 'Data.List.intersperse' \" \" [\"the\", \"quick\", \"brown\", \"fox\"]
--     let l2 = 'Data.List.intersperse' \" \" [\"jumped\", \"over\", \"the\"]
--     let l  = map 'Data.ByteString.Builder.byteString' l1 ++ ['Data.ByteString.Builder.Extra.flush'] ++ map 'Data.ByteString.Builder.byteString' l2
--     is          \<- Streams.'System.IO.Streams.fromList' l
--     (os0, grab) \<- Streams.'System.IO.Streams.listOutputStream'
--     os          \<- Streams.'builderStream' os0
--     Streams.'System.IO.Streams.connect' is os >> grab
--
-- ghci> example
-- [\"the quick brown fox\",\"\",\"jumped over the\"]
-- @
--
module System.IO.Streams.Builder
 ( -- * Blaze builder conversion
   builderStream
 , builderStreamWithBufferSize
 , unsafeBuilderStream
 ) where

------------------------------------------------------------------------------
import           Control.Monad                    (when)
import           Data.ByteString.Builder.Internal (Buffer (..), BufferRange (..), Builder, byteStringFromBuffer, defaultChunkSize, fillWithBuildStep, newBuffer, runBuilder)
import           Data.ByteString.Char8            (ByteString)
import qualified Data.ByteString.Char8            as S
import           Data.IORef                       (newIORef, readIORef, writeIORef)

------------------------------------------------------------------------------
import           System.IO.Streams.Internal       (OutputStream, makeOutputStream, write, writeTo)


------------------------------------------------------------------------------
builderStreamWithBufferFunc :: IO Buffer
                            -> OutputStream ByteString
                            -> IO (OutputStream Builder)
builderStreamWithBufferFunc mkNewBuf os = do
    ref <- newIORef Nothing
    makeOutputStream $ chunk ref
  where
    chunk ref Nothing = do
        mbuf <- readIORef ref
        case mbuf of
          -- If we existing buffer leftovers, write them to the output.
          Nothing  -> return $! ()
          Just buf -> writeBuf buf
        write Nothing os
    chunk ref (Just builder) = runStep ref $ runBuilder builder

    getBuf ref = readIORef ref >>= maybe mkNewBuf return

    bumpBuf (Buffer fp (BufferRange !_ endBuf)) endPtr =
        Buffer fp (BufferRange endPtr endBuf)

    updateBuf ref buf endPtr = writeIORef ref $! Just $! bumpBuf buf endPtr

    writeBuf buf = do
        let bs = byteStringFromBuffer buf
        when (not . S.null $ bs) $ writeTo os $! Just bs

    bufRange (Buffer _ rng) = rng

    runStep ref step = do
        buf <- getBuf ref
        fillWithBuildStep step (cDone buf) (cFull buf) (cInsert buf)
                          (bufRange buf)
      where
        cDone buf endPtr !() = updateBuf ref buf endPtr
        cFull buf !endPtr !_ newStep = do
            writeBuf $! bumpBuf buf endPtr
            writeIORef ref Nothing
            runStep ref newStep
        cInsert buf !endPtr !bs newStep = do
            writeBuf $! bumpBuf buf endPtr
            writeIORef ref Nothing
            writeTo os $! Just bs
            runStep ref newStep


------------------------------------------------------------------------------
-- | Converts a 'ByteString' sink into a 'Builder' sink, using the supplied
-- buffer size.
--
-- Note that if the generated builder receives a
-- 'Blaze.ByteString.Builder.flush', by convention it will send an empty string
-- to the supplied @'OutputStream' 'ByteString'@ to indicate that any output
-- buffers are to be flushed.
--
-- /Since: 1.3.0.0./
builderStreamWithBufferSize :: Int -> OutputStream ByteString -> IO (OutputStream Builder)
builderStreamWithBufferSize bufsiz = builderStreamWithBufferFunc (newBuffer bufsiz)


------------------------------------------------------------------------------
-- | Converts a 'ByteString' sink into a 'Builder' sink.
--
-- Note that if the generated builder receives a
-- 'Blaze.ByteString.Builder.flush', by convention it will send an empty string
-- to the supplied @'OutputStream' 'ByteString'@ to indicate that any output
-- buffers are to be flushed.
--
builderStream :: OutputStream ByteString -> IO (OutputStream Builder)
builderStream = builderStreamWithBufferSize defaultChunkSize


------------------------------------------------------------------------------
-- | Unsafe variation on 'builderStream' that reuses an existing buffer for
-- efficiency.
--
-- /NOTE/: because the buffer is reused, subsequent 'ByteString' values written
-- to the wrapped 'OutputString' will cause previous yielded strings to change.
-- Do not retain references to these 'ByteString' values inside the
-- 'OutputStream' you pass to this function, or you will violate referential
-- transparency.
--
-- If you /must/ retain copies of these values, then please use
-- 'Data.ByteString.copy' to ensure that you have a fresh copy of the
-- underlying string.
--
-- You can create a Buffer with 'Data.ByteString.Builder.Internal.newBuffer'.
--
unsafeBuilderStream :: IO Buffer
                    -> OutputStream ByteString
                    -> IO (OutputStream Builder)
unsafeBuilderStream mkBuf os = do
    buf <- mkBuf
    builderStreamWithBufferFunc (return buf) os
