{-# LANGUAGE FlexibleContexts #-}
-- | Streaming compression and decompression using conduits.
--
-- Parts of this code were taken from zlib-enum and adapted for conduits.
module Data.Conduit.Zlib (
    -- * Conduits
    compress, decompress, gzip, ungzip,
    -- * Flushing
    compressFlush, decompressFlush,
    -- * Re-exported from zlib-bindings
    WindowBits (..), defaultWindowBits
) where

import Codec.Zlib
import Data.Conduit
import Data.ByteString (ByteString)
import qualified Data.ByteString as S

-- | Gzip compression with default parameters.
gzip :: MonadUnsafeIO m => Conduit ByteString m ByteString
gzip = compress 1 (WindowBits 31)

-- | Gzip decompression with default parameters.
ungzip :: MonadUnsafeIO m => Conduit ByteString m ByteString
ungzip = decompress (WindowBits 31)

-- |
-- Decompress (inflate) a stream of 'ByteString's. For example:
--
-- >    sourceFile "test.z" $= decompress defaultWindowBits $$ sinkFile "test"

decompress
    :: MonadUnsafeIO m
    => WindowBits -- ^ Zlib parameter (see the zlib-bindings package as well as the zlib C library)
    -> Conduit ByteString m ByteString
decompress config = NeedInput
    (\input -> PipeM (do
        inf <- unsafeLiftIO $ initInflate config
        push inf input) (return ()))
    (Done Nothing ())
  where
    push' inf x = PipeM (push inf x) (return ())

    push inf x = do
        popper <- unsafeLiftIO $ feedInflate inf x
        goPopper (push' inf) (close inf) id [] popper

    close inf = flip PipeM (return ()) $ do
        chunk <- unsafeLiftIO $ finishInflate inf
        return $
            if S.null chunk
                then Done Nothing ()
                else HaveOutput (Done Nothing ()) (return ()) chunk

-- | Same as 'decompress', but allows you to explicitly flush the stream.
decompressFlush
    :: MonadUnsafeIO m
    => WindowBits -- ^ Zlib parameter (see the zlib-bindings package as well as the zlib C library)
    -> Conduit (Flush ByteString) m (Flush ByteString)
decompressFlush config = NeedInput
    (\input -> flip PipeM (return ()) $ do
        inf <- unsafeLiftIO $ initInflate config
        push inf input)
    (Done Nothing ())
  where
    push' inf x = PipeM (push inf x) (return ())

    push inf (Chunk x) = do
        popper <- unsafeLiftIO $ feedInflate inf x
        goPopper (push' inf) (close inf) Chunk [] popper
    push inf Flush = do
        chunk <- unsafeLiftIO $ flushInflate inf
        let next = HaveOutput
                (NeedInput (push' inf) (close inf))
                (return ())
                Flush
        return $
            if S.null chunk
                then next
                else HaveOutput next (return ()) (Chunk chunk)

    close inf = flip PipeM (return ()) $ do
        chunk <- unsafeLiftIO $ finishInflate inf
        return $
            if S.null chunk
                then Done Nothing ()
                else HaveOutput (Done Nothing ()) (return ()) $ Chunk chunk

-- |
-- Compress (deflate) a stream of 'ByteString's. The 'WindowBits' also control
-- the format (zlib vs. gzip).

compress
    :: MonadUnsafeIO m
    => Int         -- ^ Compression level
    -> WindowBits  -- ^ Zlib parameter (see the zlib-bindings package as well as the zlib C library)
    -> Conduit ByteString m ByteString
compress level config = NeedInput
    (\input -> flip PipeM (return ()) $ do
        def <- unsafeLiftIO $ initDeflate level config
        push def input) (Done Nothing ())
  where
    push' def input = PipeM (push def input) (return ())
    push def x = do
        popper <- unsafeLiftIO $ feedDeflate def x
        goPopper (push' def) (close def) id [] popper

    close def = slurp $ unsafeLiftIO $ finishDeflate def

-- | Same as 'compress', but allows you to explicitly flush the stream.
compressFlush
    :: MonadUnsafeIO m
    => Int         -- ^ Compression level
    -> WindowBits  -- ^ Zlib parameter (see the zlib-bindings package as well as the zlib C library)
    -> Conduit (Flush ByteString) m (Flush ByteString)
compressFlush level config = NeedInput
    (\input -> flip PipeM (return ()) $ do
        def <- unsafeLiftIO $ initDeflate level config
        push def input) (Done Nothing ())
  where
    push' def input = PipeM (push def input) (return ())

    push def (Chunk x) = do
        popper <- unsafeLiftIO $ feedDeflate def x
        goPopper (push' def) (close def) Chunk [] popper
    push def Flush = goPopper (push' def) (close def) Chunk [Flush] $ flushDeflate def

    close def = flip PipeM (return ()) $ do
        mchunk <- unsafeLiftIO $ finishDeflate def
        return $ case mchunk of
            Nothing -> Done Nothing ()
            Just chunk -> HaveOutput (Done Nothing ()) (return ()) (Chunk chunk)

goPopper :: MonadUnsafeIO m
         => (input -> Conduit input m output)
         -> Conduit input m output
         -> (S.ByteString -> output)
         -> [output]
         -> Popper
         -> m (Conduit input m output)
goPopper push close wrap final popper = do
    mbs <- unsafeLiftIO popper
    return $ case mbs of
        Nothing ->
            let go [] = NeedInput push close
                go (x:xs) = HaveOutput (go xs) (return ()) x
             in go final
        Just bs -> HaveOutput (PipeM (goPopper push close wrap final popper) (return ())) (return ()) (wrap bs)

slurp :: Monad m => m (Maybe a) -> Pipe i a m ()
slurp pop = flip PipeM (return ()) $ do
    x <- pop
    return $ case x of
        Nothing -> Done Nothing ()
        Just y -> HaveOutput (slurp pop) (return ()) y