{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE OverloadedStrings #-}
module Network.HTTP.Client.Body
    ( makeChunkedReader
    , makeLengthReader
    , makeGzipReader
    , makeUnlimitedReader
    , brConsume
    , brEmpty
    , brAddCleanup
    , brReadSome
    ) where

import Network.HTTP.Client.Connection
import Network.HTTP.Client.Types
import Control.Exception (throwIO, assert)
import Data.ByteString (ByteString, empty, uncons)
import Data.IORef
import qualified Data.ByteString as S
import qualified Data.ByteString.Lazy as L
import Control.Monad (unless, when)
import qualified Codec.Zlib as Z

brReadSome :: BodyReader -> Int -> IO L.ByteString
brReadSome BodyReader {..} =
    loop id
  where
    loop front rem
        | rem <= 0 = return $ L.fromChunks $ front []
        | otherwise = do
            bs <- brRead
            if S.null bs
                then return $ L.fromChunks $ front []
                else loop (front . (bs:)) (rem - S.length bs)

brEmpty :: BodyReader
brEmpty = BodyReader
    { brRead = return S.empty
    , brComplete = return True
    }

brAddCleanup :: IO () -> BodyReader -> BodyReader
brAddCleanup cleanup br = BodyReader
    { brRead = do
        bs <- brRead br
        when (S.null bs) cleanup
        return bs
    , brComplete = brComplete br
    }

-- | Strictly consume all remaining chunks of data from the stream.
--
-- Since 0.1.0
brConsume :: BodyReader -> IO [S.ByteString]
brConsume f =
    go id
  where
    go front = do
        x <- brRead f
        if S.null x
            then return $ front []
            else go (front . (x:))

makeGzipReader :: BodyReader -> IO BodyReader
makeGzipReader br = do
    inf <- Z.initInflate $ Z.WindowBits 31
    istate <- newIORef Nothing
    let goPopper popper = do
            mbs <- popper
            case mbs of
                Just bs -> do
                    writeIORef istate $ Just popper
                    return bs
                Nothing -> do
                    bs <- Z.flushInflate inf
                    if S.null bs
                        then start
                        else do
                            writeIORef istate Nothing
                            return bs
        start = do
            bs <- brRead br
            if S.null bs
                then return S.empty
                else do
                    popper <- Z.feedInflate inf bs
                    goPopper popper
    return BodyReader
        { brRead = do
            state <- readIORef istate
            case state of
                Nothing -> start
                Just popper -> goPopper popper
        , brComplete = brComplete br
        }

makeUnlimitedReader :: Connection -> IO BodyReader
makeUnlimitedReader Connection {..} = do
    icomplete <- newIORef False
    return $! BodyReader
        { brRead = do
            bs <- connectionRead
            when (S.null bs) $ writeIORef icomplete True
            return bs
        , brComplete = readIORef icomplete
        }

makeLengthReader :: Int -> Connection -> IO BodyReader
makeLengthReader count0 Connection {..} = do
    icount <- newIORef count0
    return $! BodyReader
        { brRead = do
            count <- readIORef icount
            if count <= 0
                then return empty
                else do
                    bs <- connectionRead
                    when (S.null bs) $ throwIO $ ResponseBodyTooShort (fromIntegral count0) (fromIntegral $ count0 - count)
                    case compare count $ S.length bs of
                        LT -> do
                            let (x, y) = S.splitAt count bs
                            connectionUnread y
                            writeIORef icount (-1)
                            return x
                        EQ -> do
                            writeIORef icount (-1)
                            return bs
                        GT -> do
                            writeIORef icount (count - S.length bs)
                            return bs
        , brComplete = fmap (== -1) $ readIORef icount
        }

makeChunkedReader :: Bool -- ^ send headers
                  -> Connection
                  -> IO BodyReader
makeChunkedReader sendHeaders conn@Connection {..} = do
    icount <- newIORef 0
    return $! BodyReader
        { brRead = go icount
        , brComplete = do
            count <- readIORef icount
            return $! count == -1
        }
  where
    go icount = do
        count0 <- readIORef icount
        count <-
            if count0 == 0
                then readHeader
                else return count0
        if count <= 0
            then do
                writeIORef icount (-1)
                return empty
            else do
                (bs, count') <- sendChunk count
                writeIORef icount count'
                return bs

    sendChunk 0 = return (empty, 0)
    sendChunk remainder = do
        bs <- connectionRead
        when (S.null bs) $ throwIO InvalidChunkHeaders
        case compare remainder $ S.length bs of
            LT -> do
                let (x, y) = S.splitAt remainder bs
                assert (not $ S.null y) $ connectionUnread y
                requireNewline
                return (x, 0)
            EQ -> do
                requireNewline
                return (bs, 0)
            GT -> return (bs, remainder - S.length bs)

    requireNewline = do
        bs <- connectionReadLine conn
        unless (S.null bs) $ throwIO InvalidChunkHeaders

    readHeader = do
        bs <- connectionReadLine conn
        case parseHex bs of
            Nothing -> throwIO InvalidChunkHeaders
            Just hex -> return hex

    parseHex bs0 =
        case uncons bs0 of
            Just (w0, bs')
                | Just i0 <- toI w0 -> Just $ parseHex' i0 bs'
            _ -> Nothing
    parseHex' i bs =
        case uncons bs of
            Just (w, bs)
                | Just i' <- toI w -> parseHex' (i * 16 + i') bs
            _ -> i

    toI w
        | 48 <= w && w <= 57  = Just $ fromIntegral w - 48
        | 65 <= w && w <= 70  = Just $ fromIntegral w - 55
        | 97 <= w && w <= 102 = Just $ fromIntegral w - 87
        | otherwise = Nothing
