{-# LANGUAGE BangPatterns       #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE OverloadedStrings  #-}
{-# LANGUAGE PackageImports     #-}
{-# LANGUAGE Rank2Types         #-}
{-# LANGUAGE ViewPatterns       #-}

module Snap.Internal.Http.Parser
  ( IRequest(..)
  , HttpParseException
  , parseRequest
  , readChunkedTransferEncoding
  , iterParser
  , parseCookie
  , parseUrlEncoded
  , strictize
  ) where


------------------------------------------------------------------------------
import           Control.Exception
import           Control.Monad (liftM)
import           Control.Monad.Trans
import           Data.Attoparsec
import           Data.Attoparsec.Enumerator
import           Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as S
import qualified Data.ByteString.Unsafe as S
import           Data.ByteString.Internal (w2c)
import qualified Data.ByteString.Lazy.Char8 as L
import           Data.Char
import           Data.Int
import           Data.Typeable
import           Prelude hiding (head, take, takeWhile)
----------------------------------------------------------------------------
import           Snap.Internal.Http.Types
import           Snap.Internal.Iteratee.Debug
import           Snap.Internal.Parsing hiding (pHeaders)
import           Snap.Iteratee hiding (map, take)


------------------------------------------------------------------------------
-- | an internal version of the headers part of an HTTP request
data IRequest = IRequest
    { iMethod :: Method
    , iRequestUri :: ByteString
    , iHttpVersion :: (Int,Int)
    , iRequestHeaders :: [(ByteString, ByteString)]
    }


------------------------------------------------------------------------------
instance Show IRequest where
    show (IRequest m u v r) =
        concat [ show m
               , " "
               , show u
               , " "
               , show v
               , " "
               , show r ]


------------------------------------------------------------------------------
data HttpParseException = HttpParseException String deriving (Typeable, Show)
instance Exception HttpParseException

------------------------------------------------------------------------------
parseRequest :: (Monad m) => Iteratee ByteString m (Maybe IRequest)
parseRequest = do
    eof <- isEOF
    if eof
      then return Nothing
      else do
        line <- pLine
        if S.null line
          then parseRequest
          else do
            let (!mStr,!s)   = bSp line
            let (!uri,!vStr) = bSp s

            !method <- methodFromString mStr

            let ver@(!_,!_) = pVer vStr

            hdrs    <- pHeaders
            return $! Just $! IRequest method uri ver hdrs

  where
    pVer s = if S.isPrefixOf "HTTP/" s
               then let (a,b) = bDot $ S.drop 5 s
                    in (read $ S.unpack a, read $ S.unpack b)
               else (1,0)

    isSp  = (== ' ')
    bSp   = splitWith isSp
    isDot = (== '.')
    bDot  = splitWith isDot


------------------------------------------------------------------------------
pLine :: (Monad m) => Iteratee ByteString m ByteString
pLine = continue $ k S.empty
  where
    k _ EOF = throwError $
              HttpParseException "parse error: expected line ending in crlf"
    k !pre (Chunks xs) =
        if S.null b
          then continue $ k a
          else yield a (Chunks [S.drop 2 b])
      where
        (!a,!b) = S.breakSubstring "\r\n" s
        !s      = S.append pre s'
        !s'     = S.concat xs


------------------------------------------------------------------------------
splitWith :: (Char -> Bool) -> ByteString -> (ByteString,ByteString)
splitWith !f !s = let (!a,!b) = S.break f s
                      !b'     = S.dropWhile f b
                  in (a, b')


------------------------------------------------------------------------------
pHeaders :: Monad m => Iteratee ByteString m [(ByteString,ByteString)]
pHeaders = do
    f <- go id
    return $! f []
  where
    go !dlistSoFar = {-# SCC "pHeaders/go" #-} do
        line <- pLine
        if S.null line
          then return dlistSoFar
          else do
            let (!k,!v) = pOne line
            vf <- pCont id
            let vs = vf []
            let !v' = S.concat (v:vs)
            go (dlistSoFar . ((k,v'):))

      where
        pOne s = let (k,v) = splitWith (== ':') s
                 in (trim k, trim v)

        isCont c = c == ' ' || c == '\t'

        pCont !dlist = do
            mbS  <- peek
            maybe (return dlist)
                  (\s -> if S.null s
                           then head >> pCont dlist
                           else if isCont $ w2c $ S.unsafeHead s
                                  then procCont dlist
                                  else return dlist)
                  mbS

        procCont !dlist = do
            line <- pLine
            let !t = trim line
            pCont (dlist . (" ":) . (t:))


------------------------------------------------------------------------------
methodFromString :: (Monad m) => ByteString -> Iteratee ByteString m Method
methodFromString "GET"     = return GET
methodFromString "POST"    = return POST
methodFromString "HEAD"    = return HEAD
methodFromString "PUT"     = return PUT
methodFromString "DELETE"  = return DELETE
methodFromString "TRACE"   = return TRACE
methodFromString "OPTIONS" = return OPTIONS
methodFromString "CONNECT" = return CONNECT
methodFromString "PATCH"   = return PATCH
methodFromString s         = return $ Method s


------------------------------------------------------------------------------
readChunkedTransferEncoding :: (MonadIO m) =>
                               Enumeratee ByteString ByteString m a
readChunkedTransferEncoding =
    chunkParserToEnumeratee $
    iterateeDebugWrapper "pGetTransferChunk" $
    iterParser pGetTransferChunk


------------------------------------------------------------------------------
chunkParserToEnumeratee :: (MonadIO m) =>
                           Iteratee ByteString m (Maybe ByteString)
                        -> Enumeratee ByteString ByteString m a
chunkParserToEnumeratee getChunk client = do
    mbB <- getChunk
    maybe finishIt sendBS mbB

  where
    sendBS s = do
        step <- lift $ runIteratee $ enumBS s client
        chunkParserToEnumeratee getChunk step

    finishIt = lift $ runIteratee $ enumEOF client


------------------------------------------------------------------------------
-- parse functions
------------------------------------------------------------------------------

------------------------------------------------------------------------------
pGetTransferChunk :: Parser (Maybe ByteString)
pGetTransferChunk = do
    !hex <- liftM unsafeFromHex $ (takeWhile (isHexDigit . w2c))
    takeTill ((== '\r') . w2c)
    crlf
    if hex <= (0 :: Int)
      then return Nothing
      else do
          x <- take hex
          crlf
          return $! Just x
