-- | This module exports facilities that allows you to encode and decode
-- streams of 'Bin.Binary' values using the @pipes@ and @pipes-parse@ libraries.

module Control.Proxy.Binary
  ( -- * Decoding
    -- $decoding
    decode
  , decodeD
    -- * Encoding
    -- $encoding
  , encode
  , encodeD
   -- * Types
  , I.DecodingError(..)
  ) where

-------------------------------------------------------------------------------

import qualified Data.ByteString               as BS
import qualified Data.ByteString.Lazy.Internal as BLI
import           Control.Monad                 (unless)
import qualified Control.Proxy                 as P
import qualified Control.Proxy.Binary.Internal as I
import qualified Control.Proxy.Parse           as Pa
import qualified Control.Proxy.Trans.Either    as P
import qualified Control.Proxy.Trans.State     as P
import qualified Data.Binary                   as Bin
import           Data.Foldable                 (mapM_)
import           Data.Function                 (fix)
import           Prelude                       hiding (mapM_)

--------------------------------------------------------------------------------
-- $decoding
--
-- There are two different 'Bin.Binary' decoding facilities exported by this
-- module, and choosing between them is easy: If you need to interleave decoding
-- with other stream effects you must use 'decode', otherwise you may use the
-- simpler 'decodeD'.

-- | Decodes one 'Bin.Binary' instance flowing downstream.
--
-- * In case of decoding errors, a 'I.DecodingError' exception is thrown in the
-- 'Pe.EitherP' proxy transformer.
--
-- * Requests more input from upstream using 'Pa.draw' when needed.
--
-- * /Do not/ use this proxy if 'Control.Proxy.ByteString.isEndOfBytes' returns
-- 'True', otherwise you may get unexpected decoding errors.
decode
  :: (P.Proxy p, Monad m, Bin.Binary r)
  => P.EitherP I.DecodingError (P.StateP [BS.ByteString] p)
     () (Maybe BS.ByteString) y' y m r
decode = do
    (er, mlo) <- P.liftP (I.parseWith Pa.draw Bin.get)
    P.liftP (mapM_ Pa.unDraw mlo)
    either P.throw return er
{-# INLINABLE decode #-}


-- | Decodes 'Bin.Binary' instances flowing downstream until end of input.
--
-- * In case of decoding errors, a 'I.DecodingError' exception is thrown in the
-- 'Pe.EitherP' proxy transformer.
--
-- * Requests more input from upstream using 'Pa.draw', when needed.
--
-- * Empty input chunks flowing downstream will be discarded.
decodeD
  :: (P.Proxy p, Monad m, Bin.Binary b)
  => ()
  -> P.Pipe (P.EitherP I.DecodingError (P.StateP [BS.ByteString] p))
     (Maybe BS.ByteString) b m ()
decodeD = \() -> loop where
    loop = do
        eof <- P.liftP isEndOfBytes
        unless eof $ decode >>= P.respond >> loop
{-# INLINABLE decodeD #-}

--------------------------------------------------------------------------------
-- $encoding
--
-- There are two different 'Bin.Binary' encoding facilities exported by this
-- module, and choosing between them is easy: If you need to interleave encoding
-- with other stream effects you must use 'encode', otherwise you may use the
-- simpler 'encodeD'.

-- | Encodes the given 'Bin.Binary' instance and sends it downstream in
-- 'BS.ByteString' chunks.
encode
  :: (P.Proxy p, Monad m, Bin.Binary x)
  => x -> p x' x () BS.ByteString m ()
encode = \x -> P.runIdentityP $ do
    BLI.foldrChunks (\e a -> P.respond e >> a) (return ()) (Bin.encode x)
{-# INLINABLE encode #-}


-- | Encodes 'Bin.Binary' instances flowing downstream, each in possibly more
-- than one 'BS.ByteString' chunk.
encodeD
  :: (P.Proxy p, Monad m, Bin.Binary a)
  => () -> P.Pipe p a BS.ByteString m r
encodeD = P.pull P./>/ encode
{-# INLINABLE encodeD #-}


--------------------------------------------------------------------------------
-- XXX: this function is here until pipes-bytestring exports it

-- | Like 'Pa.isEndOfInput', except it also consumes and discards leading
-- empty 'BS.ByteString' chunks.
isEndOfBytes
  :: (Monad m, P.Proxy p)
  => P.StateP [BS.ByteString] p () (Maybe BS.ByteString) y' y m Bool
isEndOfBytes = fix $ \loop -> do
    ma <- Pa.draw
    case ma of
      Just a
       | BS.null a -> loop
       | otherwise -> Pa.unDraw a >> return False
      Nothing      -> return True
{-# INLINABLE isEndOfBytes #-}