{-# LANGUAGE BangPatterns              #-}
{-# LANGUAGE CPP                       #-}
{-# LANGUAGE EmptyDataDecls            #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleInstances         #-}
{-# LANGUAGE ForeignFunctionInterface  #-}
{-# LANGUAGE OverloadedStrings         #-}
{-# LANGUAGE Rank2Types                #-}
{-# LANGUAGE TypeSynonymInstances      #-}

------------------------------------------------------------------------------
-- | An internal Snap module containing HTTP types.
--
-- /N.B./ this is an internal interface, please don't write user code that
-- depends on it. Most of these declarations (except for the
-- unsafe/encapsulation-breaking ones) are re-exported from "Snap.Core".
--
module Snap.Internal.Http.Types where

------------------------------------------------------------------------------
import           Control.Monad              (unless)
import           Data.ByteString            (ByteString)
import           Data.ByteString.Builder    (Builder, byteString, toLazyByteString)
import qualified Data.ByteString.Char8      as S
import qualified Data.ByteString.Lazy.Char8 as L
import           Data.CaseInsensitive       (CI)
import qualified Data.CaseInsensitive       as CI
import qualified Data.IntMap                as IM
import           Data.List                  hiding (take)
import           Data.Map                   (Map)
import qualified Data.Map                   as Map
import           Data.Maybe                 (Maybe (..), fromMaybe, maybe)
import           Data.Monoid                (mconcat)
import           Data.Time.Clock            (UTCTime)
import           Data.Time.Clock.POSIX      (utcTimeToPOSIXSeconds)
import           Data.Word                  (Word64)
import           Foreign.C.Types            (CTime (..))
import           Prelude                    (Bool (..), Eq (..), FilePath, IO, Int, Integral (..), Monad (..), Num ((-)), Ord (..), Ordering (..), Read (..), Show (..), String, fmap, fromInteger, fromIntegral, id, not, otherwise, truncate, ($), (.))
#ifdef PORTABLE
import           Prelude                    (realToFrac, ($!))
#endif
import           System.IO                  (IOMode (ReadMode), SeekMode (AbsoluteSeek), hSeek, withBinaryFile)
import           System.IO.Streams          (InputStream, OutputStream)
import qualified System.IO.Streams          as Streams
import           System.IO.Unsafe           (unsafePerformIO)

------------------------------------------------------------------------------
#ifdef PORTABLE
import           Data.Time.Clock.POSIX
import           Data.Time.Clock.POSIX
import           Data.Time.Format           as Time
import           Data.Time.Locale.Compat    (defaultTimeLocale)
import           Data.Time.LocalTime
#else
import qualified Data.ByteString.Unsafe     as S
import           Data.Time.Format           ()
import           Foreign.C.String           (CString)
import           Foreign.Marshal.Alloc      (mallocBytes)
#endif

------------------------------------------------------------------------------
import           Snap.Types.Headers         (Headers)
import qualified Snap.Types.Headers         as H


#ifndef PORTABLE

------------------------------------------------------------------------------
-- foreign imports from cbits
foreign import ccall unsafe "set_c_locale"
        set_c_locale :: IO ()

foreign import ccall unsafe "c_parse_http_time"
        c_parse_http_time :: CString -> IO CTime

foreign import ccall unsafe "c_format_http_time"
        c_format_http_time :: CTime -> CString -> IO ()

foreign import ccall unsafe "c_format_log_time"
        c_format_log_time :: CTime -> CString -> IO ()

#endif


------------------------------------------------------------------------------
-- | A typeclass for datatypes which contain HTTP headers.
class HasHeaders a where
    -- | Modify the datatype's headers.
    updateHeaders :: (Headers -> Headers) -> a -> a

    -- | Retrieve the headers from a datatype that has headers.
    headers       :: a -> Headers


------------------------------------------------------------------------------
-- | Adds a header key-value-pair to the 'HasHeaders' datatype. If a header
-- with the same name already exists, the new value is appended to the headers
-- list.
--
-- Example:
--
-- @
-- ghci> import qualified "Snap.Types.Headers" as H
-- ghci> 'addHeader' \"Host\" "localhost" H.'empty'
-- H {unH = [("host","localhost")]}
-- ghci> 'addHeader' \"Host\" "127.0.0.1" it
-- H {unH = [("host","localhost,127.0.0.1")]}
-- @
addHeader :: (HasHeaders a) => CI ByteString -> ByteString -> a -> a
addHeader k v = updateHeaders $ H.insert k v


------------------------------------------------------------------------------
-- | Sets a header key-value-pair in a 'HasHeaders' datatype. If a header with
-- the same name already exists, it is overwritten with the new value.
--
-- Example:
--
-- @
-- ghci> import qualified "Snap.Types.Headers" as H
-- ghci> 'setHeader' \"Host\" "localhost" H.'empty'
-- H {unH = [(\"host\",\"localhost\")]}
-- ghci> setHeader \"Host\" "127.0.0.1" it
-- H {unH = [("host","127.0.0.1")]}
-- @
setHeader :: (HasHeaders a) => CI ByteString -> ByteString -> a -> a
setHeader k v = updateHeaders $ H.set k v


------------------------------------------------------------------------------
-- | Gets a header value out of a 'HasHeaders' datatype.
--
-- Example:
--
-- @
-- ghci> import qualified "Snap.Types.Headers" as H
-- ghci> 'getHeader' \"Host\" $ 'setHeader' \"Host\" "localhost" H.'empty'
-- Just "localhost"
-- @
getHeader :: (HasHeaders a) => CI ByteString -> a -> Maybe ByteString
getHeader k a = H.lookup k $ headers a


------------------------------------------------------------------------------
-- | Lists all the headers out of a 'HasHeaders' datatype. If many
-- headers came in with the same name, they will be catenated together.
--
-- Example:
--
-- @
-- ghci> import qualified "Snap.Types.Headers" as H
-- ghci> 'listHeaders' $ 'setHeader' \"Host\" "localhost" H.'empty'
-- [("host","localhost")]
-- @
listHeaders :: (HasHeaders a) => a -> [(CI ByteString, ByteString)]
listHeaders = H.toList . headers


------------------------------------------------------------------------------
-- | Clears a header value from a 'HasHeaders' datatype.
--
-- Example:
--
-- @
-- ghci> import qualified "Snap.Types.Headers" as H
-- ghci> 'deleteHeader' \"Host\" $ 'setHeader' \"Host\" "localhost" H.'empty'
-- H {unH = []}
-- @
deleteHeader :: (HasHeaders a) => CI ByteString -> a -> a
deleteHeader k = updateHeaders $ H.delete k


------------------------------------------------------------------------------
-- | Enumerates the HTTP method values (see
-- <http://tools.ietf.org/html/rfc2068.html#section-5.1.1>).
data Method  = GET | HEAD | POST | PUT | DELETE | TRACE | OPTIONS | CONNECT |
               PATCH | Method ByteString
               deriving(Show, Read)

instance Eq Method where
    a == b =
        normalizeMethod a `eq` normalizeMethod b
      where
        GET       `eq` GET       = True
        HEAD      `eq` HEAD      = True
        POST      `eq` POST      = True
        PUT       `eq` PUT       = True
        DELETE    `eq` DELETE    = True
        TRACE     `eq` TRACE     = True
        OPTIONS   `eq` OPTIONS   = True
        CONNECT   `eq` CONNECT   = True
        PATCH     `eq` PATCH     = True
        Method x1 `eq` Method y1 = x1 == y1
        _         `eq` _         = False

instance Ord Method where
        compare a b =
            check (normalizeMethod a) (normalizeMethod b)
          where
            check   GET          GET           = EQ
            check   HEAD         HEAD          = EQ
            check   POST         POST          = EQ
            check   PUT          PUT           = EQ
            check   DELETE       DELETE        = EQ
            check   TRACE        TRACE         = EQ
            check   OPTIONS      OPTIONS       = EQ
            check   CONNECT      CONNECT       = EQ
            check   PATCH        PATCH         = EQ
            check   (Method  x1) (Method   y1) = compare x1 y1
            check   x            y             = compare (tag x) (tag y)

            tag :: Method -> Int
            tag (GET{})     = 0
            tag (HEAD{})    = 1
            tag (POST{})    = 2
            tag (PUT{})     = 3
            tag (DELETE{})  = 4
            tag (TRACE{})   = 5
            tag (OPTIONS{}) = 6
            tag (CONNECT{}) = 7
            tag (PATCH{})   = 8
            tag (Method{})  = 9

-- | Equate the special case constructors with their corresponding
-- @Method name@ variant.
{-# INLINE normalizeMethod #-}
normalizeMethod :: Method -> Method
normalizeMethod m@(Method name) = case name of
                                    "GET"     -> GET
                                    "HEAD"    -> HEAD
                                    "POST"    -> POST
                                    "PUT"     -> PUT
                                    "DELETE"  -> DELETE
                                    "TRACE"   -> TRACE
                                    "OPTIONS" -> OPTIONS
                                    "CONNECT" -> CONNECT
                                    "PATCH"   -> PATCH
                                    _         -> m
normalizeMethod m               = m


------------------------------------------------------------------------------
-- | Represents a (major, minor) version of the HTTP protocol.
type HttpVersion = (Int,Int)


------------------------------------------------------------------------------
-- | A datatype representing an HTTP cookie.
data Cookie = Cookie {
      -- | The name of the cookie.
      cookieName     :: !ByteString

      -- | The cookie's string value.
    , cookieValue    :: !ByteString

      -- | The cookie's expiration value, if it has one.
    , cookieExpires  :: !(Maybe UTCTime)

      -- | The cookie's \"domain\" value, if it has one.
    , cookieDomain   :: !(Maybe ByteString)

      -- | The cookie path.
    , cookiePath     :: !(Maybe ByteString)

      -- | Tag as secure cookie?
    , cookieSecure   :: !Bool

      -- | HTTP only?
    , cookieHttpOnly :: !Bool
} deriving (Eq, Show)


------------------------------------------------------------------------------
-- | A type alias for the HTTP parameters mapping. Each parameter
-- key maps to a list of 'ByteString' values; if a parameter is specified
-- multiple times (e.g.: \"@GET /foo?param=bar1&param=bar2@\"), looking up
-- \"@param@\" in the mapping will give you @[\"bar1\", \"bar2\"]@.
type Params = Map ByteString [ByteString]


------------------------------------------------------------------------------
-- request type
------------------------------------------------------------------------------

------------------------------------------------------------------------------
-- | Contains all of the information about an incoming HTTP request.
data Request = Request
    { -- | The server name of the request, as it came in from the request's
      -- @Host:@ header.
      --
      -- Example:
      --
      -- @
      -- ghci> :set -XOverloadedStrings
      -- ghci> import qualified "Snap.Test" as T
      -- ghci> import qualified "Data.Map" as M
      -- ghci> :{
      -- ghci| rq <- T.buildRequest $ do
      -- ghci|         T.get "\/foo\/bar" M.empty
      -- ghci|         T.setHeader "host" "example.com"
      -- ghci| :}
      -- ghci> rqHostName rq
      -- "example.com"
      -- @
      rqHostName      :: ByteString

      -- | The remote IP address.
      --
      -- Example:
      --
      -- @
      -- ghci> :set -XOverloadedStrings
      -- ghci> import qualified "Snap.Test" as T
      -- ghci> import qualified "Data.Map" as M
      -- ghci> rqClientAddr \`fmap\` T.buildRequest (T.get "\/foo\/bar" M.empty)
      -- "127.0.0.1"
      -- @
    , rqClientAddr    :: ByteString

      -- | The remote TCP port number.
      --
      -- Example:
      --
      -- @
      -- ghci> :set -XOverloadedStrings
      -- ghci> import qualified "Snap.Test" as T
      -- ghci> import qualified "Data.Map" as M
      -- ghci> rqClientPort \`fmap\` T.buildRequest (T.get "\/foo\/bar" M.empty)
      -- "60000"
      -- @
    , rqClientPort    :: {-# UNPACK #-} !Int

      -- | The local IP address for this request.
      --
      -- Example:
      --
      -- @
      -- ghci> :set -XOverloadedStrings
      -- ghci> import qualified "Snap.Test" as T
      -- ghci> import qualified "Data.Map" as M
      -- ghci> rqServerAddr \`fmap\` T.buildRequest (T.get "\/foo\/bar" M.empty)
      -- "127.0.0.1"
      -- @
    , rqServerAddr    :: ByteString

      -- | Returns the port number the HTTP server is listening on. This may be
      -- useless from the perspective of external requests, e.g. if the server
      -- is running behind a proxy.
      --
      -- Example:
      --
      -- @
      -- ghci> :set -XOverloadedStrings
      -- ghci> import qualified "Snap.Test" as T
      -- ghci> import qualified "Data.Map" as M
      -- ghci> rqServerPort \`fmap\` T.buildRequest (T.get "\/foo\/bar" M.empty)
      -- 8080
      -- @
    , rqServerPort    :: {-# UNPACK #-} !Int

      -- | Returns the HTTP server's idea of its local hostname, including
      -- port. This is as configured with the @Config@ object at startup.
      --
      -- Example:
      --
      -- @
      -- ghci> :set -XOverloadedStrings
      -- ghci> import qualified "Snap.Test" as T
      -- ghci> import qualified "Data.Map" as M
      -- ghci> rqLocalHostname \`fmap\` T.buildRequest (T.get "\/foo\/bar" M.empty)
      -- "localhost"
      -- @
    , rqLocalHostname :: ByteString

      -- | Returns @True@ if this is an HTTPS session.
      --
      -- Example:
      --
      -- @
      -- ghci> :set -XOverloadedStrings
      -- ghci> import qualified "Snap.Test" as T
      -- ghci> import qualified "Data.Map" as M
      -- ghci> rqIsSecure \`fmap\` T.buildRequest (T.get "\/foo\/bar" M.empty)
      -- False
      -- @
    , rqIsSecure      :: !Bool

      -- | Contains all HTTP 'Headers' associated with this request.
      --
      -- Example:
      --
      -- @
      -- ghci> :set -XOverloadedStrings
      -- ghci> import qualified "Snap.Test" as T
      -- ghci> import qualified "Data.Map" as M
      -- ghci> rqHeaders \`fmap\` T.buildRequest (T.get "\/foo\/bar" M.empty)
      -- H {unH = [("host","localhost")]}
      -- @
    , rqHeaders       :: Headers

      -- | Actual body of the request.
    , rqBody          :: InputStream ByteString

      -- | Returns the @Content-Length@ of the HTTP request body.
      --
      -- Example:
      --
      -- @
      -- ghci> :set -XOverloadedStrings
      -- ghci> import qualified "Snap.Test" as T
      -- ghci> import qualified "Data.Map" as M
      -- ghci> rqContentLength \`fmap\` T.buildRequest (T.get "\/foo\/bar" M.empty)
      -- Nothing
      -- @
    , rqContentLength :: !(Maybe Word64)

      -- | Returns the HTTP request method.
      --
      -- Example:
      --
      -- @
      -- ghci> :set -XOverloadedStrings
      -- ghci> import qualified "Snap.Test" as T
      -- ghci> import qualified "Data.Map" as M
      -- ghci> rqMethod \`fmap\` T.buildRequest (T.get "\/foo\/bar" M.empty)
      -- GET
      -- @
    , rqMethod        :: !Method

      -- | Returns the HTTP version used by the client.
      --
      -- Example:
      --
      -- @
      -- ghci> :set -XOverloadedStrings
      -- ghci> import qualified "Snap.Test" as T
      -- ghci> import qualified "Data.Map" as M
      -- ghci> rqVersion \`fmap\` T.buildRequest (T.get "\/foo\/bar" M.empty)
      -- (1,1)
      -- @
    , rqVersion       :: {-# UNPACK #-} !HttpVersion

      -- | Returns a list of the cookies that came in from the HTTP request
      -- headers.
      --
      -- Example:
      --
      -- @
      -- ghci> :set -XOverloadedStrings
      -- ghci> import qualified "Snap.Test" as T
      -- ghci> import qualified "Data.Map" as M
      -- ghci> rqCookies \`fmap\` T.buildRequest (T.get "\/foo\/bar" M.empty)
      -- []
      -- @
    , rqCookies       :: [Cookie]

      -- | Handlers can be hung on a @URI@ \"entry point\"; this is called the
      -- \"context path\". If a handler is hung on the context path
      -- @\"\/foo\/\"@, and you request @\"\/foo\/bar\"@, the value of
      -- 'rqPathInfo' will be @\"bar\"@.
      --
      -- The following identity holds:
      --
      -- > rqURI r == S.concat [ rqContextPath r
      -- >                     , rqPathInfo r
      -- >                     , let q = rqQueryString r
      -- >                       in if S.null q
      -- >                            then ""
      -- >                            else S.append "?" q
      -- >                     ]
      --
      -- Example:
      --
      -- @
      -- ghci> :set -XOverloadedStrings
      -- ghci> import qualified "Snap.Test" as T
      -- ghci> import qualified "Data.Map" as M
      -- ghci> rqPathInfo \`fmap\` T.buildRequest (T.get "\/foo\/bar" M.empty)
      -- "foo/bar"
      -- @
    , rqPathInfo      :: ByteString

      -- | The \"context path\" of the request; catenating 'rqContextPath',
      -- and 'rqPathInfo' should get you back to the original 'rqURI'
      -- (ignoring query strings). The 'rqContextPath' always begins and ends
      -- with a slash (@\"\/\"@) character, and represents the path (relative
      -- to your component\/snaplet) you took to get to your handler.
      --
      -- Example:
      --
      -- @
      -- ghci> :set -XOverloadedStrings
      -- ghci> import qualified "Snap.Test" as T
      -- ghci> import qualified "Data.Map" as M
      -- ghci> rqContextPath \`fmap\` T.buildRequest (T.get "\/foo\/bar" M.empty)
      -- "/"
      -- @
    , rqContextPath   :: ByteString

      -- | Returns the @URI@ requested by the client.
      --
      -- Example:
      --
      -- @
      -- ghci> :set -XOverloadedStrings
      -- ghci> import qualified "Snap.Test" as T
      -- ghci> import qualified "Data.Map" as M
      -- ghci> rqURI \`fmap\` T.buildRequest (T.get "\/foo\/bar" M.empty)
      -- "foo/bar"
      -- @
    , rqURI           :: ByteString

      -- | Returns the HTTP query string for this 'Request'.
      --
      -- Example:
      --
      -- @
      -- ghci> :set -XOverloadedStrings
      -- ghci> import qualified "Snap.Test" as T
      -- ghci> import qualified "Data.Map" as M
      -- ghci> rq <- T.buildRequest (T.get "\/foo\/bar" (M.fromList [("name", ["value"])]))
      -- ghci> rqQueryString rq
      -- "name=value"
      -- @
    , rqQueryString   :: ByteString

      -- | Returns the parameters mapping for this 'Request'. \"Parameters\"
      -- are automatically decoded from the URI's query string and @POST@ body
      -- and entered into this mapping. The 'rqParams' value is thus a union of
      -- 'rqQueryParams' and 'rqPostParams'.
      --
      -- Example:
      --
      -- @
      -- ghci> :set -XOverloadedStrings
      -- ghci> import qualified "Snap.Test" as T
      -- ghci> import qualified "Data.Map" as M
      -- ghci> :{
      -- ghci| rq <- T.buildRequest $ do
      -- ghci|         T.postUrlEncoded "\/foo\/bar" $ M.fromList [("baz", ["qux"])]
      -- ghci|         T.setQueryStringRaw "baz=quux"
      -- ghci| :}
      -- ghci> rqParams rq
      -- fromList [("baz",["qux","quux"])]
      -- @
    , rqParams        :: Params

      -- | The parameter mapping decoded from the URI's query string.
      --
      -- Example:
      --
      -- @
      -- ghci> :set -XOverloadedStrings
      -- ghci> import qualified "Snap.Test" as T
      -- ghci> import qualified "Data.Map" as M
      -- ghci> :{
      -- ghci| rq <- T.buildRequest $ do
      -- ghci|         T.postUrlEncoded "\/foo\/bar" $ M.fromList [("baz", ["qux"])]
      -- ghci|         T.setQueryStringRaw "baz=quux"
      -- ghci| :}
      -- ghci> rqQueryParams rq
      -- fromList [("baz",["quux"])]
      -- @
    , rqQueryParams   :: Params

      -- | The parameter mapping decoded from the POST body. Note that Snap
      -- only auto-decodes POST request bodies when the request's
      -- @Content-Type@ is @application\/x-www-form-urlencoded@.
      -- For @multipart\/form-data@ use 'Snap.Util.FileUploads.handleFileUploads'
      -- to decode the POST request and fill this mapping.
      --
      -- Example:
      --
      -- @
      -- ghci> :set -XOverloadedStrings
      -- ghci> import qualified "Snap.Test" as T
      -- ghci> import qualified "Data.Map" as M
      -- ghci> :{
      -- ghci| rq <- T.buildRequest $ do
      -- ghci|         T.postUrlEncoded "\/foo\/bar" $ M.fromList [("baz", ["qux"])]
      -- ghci|         T.setQueryStringRaw "baz=quux"
      -- ghci| :}
      -- ghci> rqPostParams rq
      -- fromList [("baz",["qux"])]
      -- @
    , rqPostParams    :: Params
    }


------------------------------------------------------------------------------
instance Show Request where
  show r = concat [ method, " ", uri, " HTTP/", version, "\n"
                  , hdrs, "\n\n"
                  , "sn=\"", sname, "\" c=", clntAddr, " s=", srvAddr
                  , " ctx=", contextpath, " clen=", contentlength, secure
                  , params, cookies
                  ]
    where
      method        = show $ rqMethod r
      uri           = S.unpack $ rqURI r
      version       = let (mj, mn) = rqVersion r in show mj ++ "." ++ show mn
      hdrs          = intercalate "\n" $ map showHdr (H.toList $ rqHeaders r)
      showHdr (a,b) = (S.unpack $ CI.original a) ++ ": " ++ S.unpack b
      sname         = S.unpack $ rqLocalHostname r
      clntAddr      = concat [S.unpack $ rqClientAddr r, ":", show $ rqClientPort r]
      srvAddr       = concat [S.unpack $ rqServerAddr r, ":", show $ rqServerPort r]
      contextpath   = S.unpack $ rqContextPath r
      contentlength = maybe "n/a" show (rqContentLength r)
      secure        = if rqIsSecure r then " secure" else ""

      params        = showFlds "\nparams: " ", " $
                      map (\ (a,b) -> S.unpack a ++ ": " ++ show b)
                      (Map.toAscList $ rqParams r)
      cookies       = showFlds "\ncookies: " "\n         " $
                      map show (rqCookies r)

      showFlds header delim lst
                    = if not . null $ lst then header ++ (intercalate delim lst)
                      else "" :: String

------------------------------------------------------------------------------
instance HasHeaders Request where
    headers           = rqHeaders
    updateHeaders f r = r { rqHeaders = f (rqHeaders r) }


------------------------------------------------------------------------------
instance HasHeaders Headers where
    headers       = id
    updateHeaders = id

------------------------------------------------------------------------------
-- response type
------------------------------------------------------------------------------

type StreamProc = OutputStream Builder -> IO (OutputStream Builder)
data ResponseBody = Stream (StreamProc)
                      -- ^ output body is a function that writes to a 'Builder'
                      -- stream

                  | SendFile FilePath (Maybe (Word64, Word64))
                      -- ^ output body is sendfile(), optional second argument
                      --   is a byte range to send


------------------------------------------------------------------------------
rspBodyMap :: (StreamProc -> StreamProc) -> ResponseBody -> ResponseBody
rspBodyMap f b = Stream $ f $ rspBodyToEnum b


------------------------------------------------------------------------------
rspBodyToEnum :: ResponseBody -> StreamProc
rspBodyToEnum (Stream e) = e

rspBodyToEnum (SendFile fp Nothing) = \out ->
    Streams.withFileAsInput fp $ \is -> do
        is' <- Streams.mapM (return . byteString) is
        Streams.connect is' out
        return out

rspBodyToEnum (SendFile fp (Just (start, end))) = \out ->
    withBinaryFile fp ReadMode $ \handle -> do
        unless (start == 0) $ hSeek handle AbsoluteSeek $ toInteger start
        is  <- Streams.handleToInputStream handle
        is' <- Streams.takeBytes (fromIntegral $ end - start) is >>=
               Streams.mapM (return . byteString)
        Streams.connect is' out
        return out


------------------------------------------------------------------------------
-- | Represents an HTTP response.
data Response = Response
    { rspHeaders            :: Headers
    , rspCookies            :: Map ByteString Cookie

      -- | We will need to inspect the content length no matter what, and
      --   looking up \"content-length\" in the headers and parsing the number
      --   out of the text will be too expensive.
    , rspContentLength      :: !(Maybe Word64)
    , rspBody               :: ResponseBody

      -- | Returns the HTTP status code.
      --
      -- Example:
      --
      -- @
      -- ghci> rspStatus 'emptyResponse'
      -- 200
      -- @
    , rspStatus             :: !Int

      -- | Returns the HTTP status explanation string.
      --
      -- Example:
      --
      -- @
      -- ghci> rspStatusReason 'emptyResponse'
      -- "OK"
      -- @
    , rspStatusReason       :: !ByteString

      -- | If true, we are transforming the request body with
      -- 'transformRequestBody'
    , rspTransformingRqBody :: !Bool
    }


------------------------------------------------------------------------------
instance Show Response where
  show r = concat [ statusline
                  , hdrs
                  , contentLength
                  , "\r\n"
                  , body
                  ]
    where
      statusline = concat [ "HTTP/1.1 "
                          , show $ rspStatus r
                          , " "
                          , S.unpack $ rspStatusReason r
                          , "\r\n" ]

      hdrs = concatMap showHdr $ H.toList $ renderCookies r
             $ rspHeaders $ clearContentLength r

      contentLength = maybe "" (\l -> concat ["Content-Length: ", show l, "\r\n"] ) (rspContentLength r)

      showHdr (k,v) = concat [ S.unpack (CI.original k), ": ", S.unpack v, "\r\n" ]

      -- io-streams are impure, so we're forced to use 'unsafePerformIO'.
      body = unsafePerformIO $ do
        (os, grab) <- Streams.listOutputStream
        let f = rspBodyToEnum $ rspBody r
        _ <- f os
        fmap (L.unpack . toLazyByteString . mconcat) grab



------------------------------------------------------------------------------
instance HasHeaders Response where
    headers = rspHeaders
    updateHeaders f r = r { rspHeaders = f (rspHeaders r) }


------------------------------------------------------------------------------
-- | Looks up the value(s) for the given named parameter. Parameters initially
-- come from the request's query string and any decoded POST body (if the
-- request's @Content-Type@ is @application\/x-www-form-urlencoded@).
-- Parameter values can be modified within handlers using "rqModifyParams".
--
-- Example:
--
-- @
-- ghci> :set -XOverloadedStrings
-- ghci> import qualified "Snap.Test" as T
-- ghci> import qualified "Data.Map" as M
-- ghci> :{
-- ghci| rq <- T.buildRequest $ do
-- ghci|         T.postUrlEncoded "\/foo\/bar" $ M.fromList [("baz", ["qux"])]
-- ghci|         T.setQueryStringRaw "baz=quux"
-- ghci| :}
-- ghci> 'rqParam' "baz" rq
-- Just ["qux","quux"]
-- @
rqParam :: ByteString           -- ^ parameter name to look up
        -> Request              -- ^ HTTP request
        -> Maybe [ByteString]
rqParam k rq = Map.lookup k $ rqParams rq
{-# INLINE rqParam #-}


------------------------------------------------------------------------------
-- | Looks up the value(s) for the given named parameter in the POST parameters
-- mapping.
--
-- Example:
--
-- @
-- ghci> :set -XOverloadedStrings
-- ghci> import qualified "Snap.Test" as T
-- ghci> import qualified "Data.Map" as M
-- ghci> :{
-- ghci| rq <- T.buildRequest $ do
-- ghci|         T.postUrlEncoded "\/foo\/bar" $ M.fromList [("baz", ["qux"])]
-- ghci|         T.setQueryStringRaw "baz=quux"
-- ghci| :}
-- ghci> 'rqPostParam' "baz" rq
-- Just ["qux"]
-- @
rqPostParam :: ByteString           -- ^ parameter name to look up
            -> Request              -- ^ HTTP request
            -> Maybe [ByteString]
rqPostParam k rq = Map.lookup k $ rqPostParams rq
{-# INLINE rqPostParam #-}


------------------------------------------------------------------------------
-- | Looks up the value(s) for the given named parameter in the query
-- parameters mapping.
--
-- Example:
--
-- @
-- ghci> :set -XOverloadedStrings
-- ghci> import qualified "Snap.Test" as T
-- ghci> import qualified "Data.Map" as M
-- ghci> :{
-- ghci| rq <- T.buildRequest $ do
-- ghci|         T.postUrlEncoded "\/foo\/bar" $ M.fromList [("baz", ["qux"])]
-- ghci|         T.setQueryStringRaw "baz=quux"
-- ghci| :}
-- ghci> 'rqQueryParam' "baz" rq
-- Just ["quux"]
-- @
rqQueryParam :: ByteString           -- ^ parameter name to look up
             -> Request              -- ^ HTTP request
             -> Maybe [ByteString]
rqQueryParam k rq = Map.lookup k $ rqQueryParams rq
{-# INLINE rqQueryParam #-}


------------------------------------------------------------------------------
-- | Modifies the parameters mapping (which is a @Map ByteString ByteString@)
-- in a 'Request' using the given function.
--
-- Example:
--
-- @
-- ghci> :set -XOverloadedStrings
-- ghci> import qualified "Snap.Test" as T
-- ghci> import qualified "Data.Map" as M
-- ghci> :{
-- ghci| rq <- T.buildRequest $ do
-- ghci|         T.postUrlEncoded "\/foo\/bar" $ M.fromList [("baz", ["qux"])]
-- ghci|         T.setQueryStringRaw "baz=quux"
-- ghci| :}
-- ghci> 'rqParams' rq
-- fromList [("baz",["qux","quux"])]
-- ghci> 'rqParams' $ 'rqModifyParams' (M.delete "baz") rq
-- fromList []
-- @
rqModifyParams :: (Params -> Params) -> Request -> Request
rqModifyParams f r = r { rqParams = p }
  where
    p = f $ rqParams r
{-# INLINE rqModifyParams #-}


------------------------------------------------------------------------------
-- | Writes a key-value pair to the parameters mapping within the given
-- request.
--
-- Example:
--
-- @
-- ghci> :set -XOverloadedStrings
-- ghci> import qualified "Snap.Test" as T
-- ghci> import qualified "Data.Map" as M
-- ghci> :{
-- ghci| rq <- T.buildRequest $ do
-- ghci|         T.postUrlEncoded "\/foo\/bar" $ M.fromList [("baz", ["qux"])]
-- ghci|         T.setQueryStringRaw "baz=quux"
-- ghci| :}
-- ghci> 'rqParams' rq
-- fromList [("baz",["qux","quux"])]
-- ghci> 'rqParams' $ 'rqSetParam' "baz" ["corge"] rq
-- fromList [("baz", ["corge"])]
-- @
rqSetParam :: ByteString        -- ^ parameter name
           -> [ByteString]      -- ^ parameter values
           -> Request           -- ^ request
           -> Request
rqSetParam k v = rqModifyParams $ Map.insert k v
{-# INLINE rqSetParam #-}


                                ---------------
                                -- responses --
                                ---------------

------------------------------------------------------------------------------
-- | An empty 'Response'.
--
-- Example:
--
-- @
-- ghci> 'emptyResponse'
-- HTTP\/1.1 200 OK
--
--
-- @
emptyResponse :: Response
emptyResponse = Response H.empty Map.empty Nothing
                         (Stream (return . id))
                         200 "OK" False


------------------------------------------------------------------------------
-- | Sets an HTTP response body to the given stream procedure.
--
-- Example:
--
-- @
-- ghci> :set -XOverloadedStrings
-- ghci> import qualified "System.IO.Streams" as Streams
-- ghci> import qualified "Data.ByteString.Builder" as Builder
-- ghci> :{
-- ghci| let r = 'setResponseBody'
-- ghci|         (\out -> do
-- ghci|             Streams.write (Just $ Builder.'byteString' \"Hello, world!\") out
-- ghci|             return out)
-- ghci|         'emptyResponse'
-- ghci| :}
-- ghci> r
-- HTTP\/1.1 200 OK
--
-- Hello, world!
-- @
setResponseBody     :: (OutputStream Builder -> IO (OutputStream Builder))
                                   -- ^ new response body
                    -> Response    -- ^ response to modify
                    -> Response
setResponseBody e r = r { rspBody = Stream e }
{-# INLINE setResponseBody #-}


------------------------------------------------------------------------------
-- | Sets the HTTP response status. Note: normally you would use
-- 'setResponseCode' unless you needed a custom response explanation.
--
--
-- Example:
--
-- @
-- ghci> :set -XOverloadedStrings
-- ghci> setResponseStatus 500 \"Internal Server Error\" 'emptyResponse'
-- HTTP\/1.1 500 Internal Server Error
--
--
-- @
setResponseStatus   :: Int        -- ^ HTTP response integer code
                    -> ByteString -- ^ HTTP response explanation
                    -> Response   -- ^ Response to be modified
                    -> Response
setResponseStatus s reason r = r { rspStatus=s, rspStatusReason=reason }
{-# INLINE setResponseStatus #-}


------------------------------------------------------------------------------
-- | Sets the HTTP response code.
--
-- Example:
--
-- @
-- ghci> setResponseCode 404 'emptyResponse'
-- HTTP\/1.1 404 Not Found
--
--
-- @
setResponseCode   :: Int        -- ^ HTTP response integer code
                  -> Response   -- ^ Response to be modified
                  -> Response
setResponseCode s r = setResponseStatus s reason r
  where
    reason = fromMaybe "Unknown" (IM.lookup s statusReasonMap)
{-# INLINE setResponseCode #-}


------------------------------------------------------------------------------
-- | Modifies a response body.
--
-- Example:
--
-- @
-- ghci> :set -XOverloadedStrings
-- ghci> import qualified "System.IO.Streams" as Streams
-- ghci> import qualified "Data.ByteString.Builder" as Builder
-- ghci> :{
-- ghci| let r = 'setResponseBody'
-- ghci|         (\out -> do
-- ghci|             Streams.write (Just $ Builder.'byteString' \"Hello, world!\") out
-- ghci|             return out)
-- ghci|         'emptyResponse'
-- ghci| :}
-- ghci> r
-- HTTP\/1.1 200 OK
--
-- Hello, world!
-- ghci> :{
-- ghci| let r' = 'modifyResponseBody'
-- ghci|          (\f out -> do
-- ghci|              out' <- f out
-- ghci|              Streams.write (Just $ Builder.'byteString' \"\\nBye, world!\") out'
-- ghci|              return out') r
-- ghci| :}
-- ghci> r'
-- HTTP\/1.1 200 OK
--
-- Hello, world!
-- Bye, world!
-- @
modifyResponseBody  :: ((OutputStream Builder -> IO (OutputStream Builder)) ->
                        (OutputStream Builder -> IO (OutputStream Builder)))
                    -> Response
                    -> Response
modifyResponseBody f r = r { rspBody = rspBodyMap f (rspBody r) }
{-# INLINE modifyResponseBody #-}


------------------------------------------------------------------------------
-- | Sets the @Content-Type@ in the 'Response' headers.
--
-- Example:
--
-- @
-- ghci> :set -XOverloadedStrings
-- ghci> setContentType \"text\/html\" 'emptyResponse'
-- HTTP\/1.1 200 OK
-- content-type: text\/html
--
--
-- @
setContentType      :: ByteString -> Response -> Response
setContentType = setHeader "Content-Type"
{-# INLINE setContentType #-}


------------------------------------------------------------------------------
-- | Convert 'Cookie' into 'ByteString' for output.
--
-- TODO: Remove duplication. This function is copied from
-- snap-server/Snap.Internal.Http.Server.Session.
cookieToBS :: Cookie -> ByteString
cookieToBS (Cookie k v mbExpTime mbDomain mbPath isSec isHOnly) = cookie
  where
    cookie = S.concat [k, "=", v, path, exptime, domain, secure, hOnly]
    path = maybe "" (S.append "; path=") mbPath
    domain = maybe "" (S.append "; domain=") mbDomain
    exptime = maybe "" (S.append "; expires=" . fmt) mbExpTime
    secure = if isSec then "; Secure" else ""
    hOnly = if isHOnly then "; HttpOnly" else ""

    -- TODO: 'formatHttpTime' uses "DD MMM YYYY" instead of "DD-MMM-YYYY",
    -- unlike the code in 'Snap.Internal.Http.Server.Session'. Is this form
    -- allowed?
    fmt = unsafePerformIO . formatHttpTime . toCTime

    toCTime :: UTCTime -> CTime
    toCTime = fromInteger . truncate . utcTimeToPOSIXSeconds

------------------------------------------------------------------------------
-- | Render cookies from a given 'Response' to 'Headers'.
--
-- TODO: Remove duplication. This function is copied from
-- snap-server/Snap.Internal.Http.Server.Session.
renderCookies :: Response -> Headers -> Headers
renderCookies r hdrs
    | null cookies = hdrs
    | otherwise = foldl' (\m v -> H.unsafeInsert "set-cookie" v m) hdrs cookies

  where
    cookies = fmap cookieToBS . Map.elems $ rspCookies r

------------------------------------------------------------------------------
-- | Adds an HTTP 'Cookie' to 'Response' headers.
--
-- Example:
--
-- @
-- ghci> :set -XOverloadedStrings
-- ghci> let cookie = 'Cookie' \"name\" \"value\" Nothing Nothing Nothing False False
-- ghci> 'getResponseCookie' \"name\" $ 'addResponseCookie' cookie 'emptyResponse'
-- Just (Cookie {cookieName = \"name\", cookieValue = \"value\", ...})
-- @
addResponseCookie :: Cookie            -- ^ cookie value
                  -> Response          -- ^ response to modify
                  -> Response
addResponseCookie ck@(Cookie k _ _ _ _ _ _) r = r { rspCookies = cks' }
  where
    cks'= Map.insert k ck $ rspCookies r
{-# INLINE addResponseCookie #-}


------------------------------------------------------------------------------
-- | Gets an HTTP 'Cookie' with the given name from 'Response' headers.
--
-- Example:
--
-- @
-- ghci> :set -XOverloadedStrings
-- ghci> 'getResponseCookie' \"cookie-name\" 'emptyResponse'
-- Nothing
-- @
getResponseCookie :: ByteString            -- ^ cookie name
                  -> Response              -- ^ response to query
                  -> Maybe Cookie
getResponseCookie cn r = Map.lookup cn $ rspCookies r
{-# INLINE getResponseCookie #-}


-- | Returns a list of 'Cookie's present in 'Response'
--
-- Example:
--
-- @
-- ghci> 'getResponseCookies' 'emptyResponse'
-- []
-- @
getResponseCookies :: Response              -- ^ response to query
                   -> [Cookie]
getResponseCookies = Map.elems . rspCookies
{-# INLINE getResponseCookies #-}


------------------------------------------------------------------------------
-- | Deletes an HTTP 'Cookie' from the 'Response' headers. Please note
-- this does not necessarily erase the cookie from the client browser.
--
-- Example:
--
-- @
-- ghci> :set -XOverloadedStrings
-- ghci> let cookie = 'Cookie' \"name\" \"value\" Nothing Nothing Nothing False False
-- ghci> let rsp    = 'addResponseCookie' cookie 'emptyResponse'
-- ghci> 'getResponseCookie' \"name\" rsp
-- Just (Cookie {cookieName = \"name\", cookieValue = \"value\", ...})
-- ghci> 'getResponseCookie' \"name\" $ 'deleteResponseCookie' \"name\" rsp
-- Nothing
-- @
deleteResponseCookie :: ByteString        -- ^ cookie name
                     -> Response          -- ^ response to modify
                     -> Response
deleteResponseCookie cn r = r { rspCookies = cks' }
  where
    cks'= Map.delete cn $ rspCookies r
{-# INLINE deleteResponseCookie #-}


------------------------------------------------------------------------------
-- | Modifies an HTTP 'Cookie' with given name in 'Response' headers.
-- Nothing will happen if a matching 'Cookie' can not be found in 'Response'.
--
-- Example:
--
-- @
-- ghci> :set -XOverloadedStrings
-- ghci> import "Data.Monoid"
-- ghci> let cookie = 'Cookie' \"name\" \"value\" Nothing Nothing Nothing False False
-- ghci> let rsp    = 'addResponseCookie' cookie 'emptyResponse'
-- ghci> 'getResponseCookie' \"name\" rsp
-- Just (Cookie {cookieName = \"name\", cookieValue = \"value\", ...})
-- ghci> let f ck@('Cookie' { cookieName = name }) = ck { cookieName = name <> \"\'\"}
-- ghci> let rsp' = 'modifyResponseCookie' \"name\" f rsp
-- ghci> 'getResponseCookie' \"name\'\" rsp\'
-- Just (Cookie {cookieName = \"name\'\", ...})
-- ghci> 'getResponseCookie' \"name\" rsp\'
-- Just (Cookie {cookieName = \"name\", ...})
-- @
modifyResponseCookie :: ByteString          -- ^ cookie name
                     -> (Cookie -> Cookie)  -- ^ modifier function
                     -> Response            -- ^ response to modify
                     -> Response
modifyResponseCookie cn f r = maybe r modify $ getResponseCookie cn r
  where
    modify ck = addResponseCookie (f ck) r
{-# INLINE modifyResponseCookie #-}


------------------------------------------------------------------------------
-- | A note here: if you want to set the @Content-Length@ for the response,
-- Snap forces you to do it with this function rather than by setting it in
-- the headers; the @Content-Length@ in the headers will be ignored.
--
-- The reason for this is that Snap needs to look up the value of
-- @Content-Length@ for each request, and looking the string value up in the
-- headers and parsing the number out of the text will be too expensive.
--
-- If you don't set a content length in your response, HTTP keep-alive will be
-- disabled for HTTP\/1.0 clients, forcing a @Connection: close@. For
-- HTTP\/1.1 clients, Snap will switch to the chunked transfer encoding if
-- @Content-Length@ is not specified.
--
-- Example:
--
-- @
-- ghci> setContentLength 400 'emptyResponse'
-- HTTP\/1.1 200 OK
-- Content-Length: 400
--
--
-- @
setContentLength    :: Word64 -> Response -> Response
setContentLength !l r = r { rspContentLength = Just l }
{-# INLINE setContentLength #-}


------------------------------------------------------------------------------
-- | Removes any @Content-Length@ set in the 'Response'.
--
-- Example:
--
-- @
-- ghci> clearContentLength $ 'setContentLength' 400 'emptyResponse'
-- HTTP\/1.1 200 OK
--
--
-- @
clearContentLength :: Response -> Response
clearContentLength r = r { rspContentLength = Nothing }
{-# INLINE clearContentLength #-}


                               ----------------
                               -- HTTP dates --
                               ----------------

------------------------------------------------------------------------------
-- | Convert a 'CTime' into an HTTP timestamp.
--
-- Example:
--
-- @
-- ghci> 'formatHttpTime' . 'fromIntegral' $ 10
-- \"Thu, 01 Jan 1970 00:00:10 GMT\"
-- @
formatHttpTime :: CTime -> IO ByteString


------------------------------------------------------------------------------
-- | Convert a 'CTime' into common log entry format.
formatLogTime :: CTime -> IO ByteString


------------------------------------------------------------------------------
-- | Converts an HTTP timestamp into a 'CTime'.
--
-- If the given time string is unparseable, this function will return 0.
--
-- Example:
--
-- @
-- ghci> :set -XOverloadedStrings
-- ghci> 'parseHttpTime' \"Thu, 01 Jan 1970 00:00:10 GMT\"
-- 10
-- @
parseHttpTime :: ByteString -> IO CTime

#ifdef PORTABLE

------------------------------------------------------------------------------
-- local definitions
fromStr :: String -> ByteString
fromStr = S.pack                -- only because we know there's no unicode
{-# INLINE fromStr #-}


------------------------------------------------------------------------------
formatHttpTime = return . format . toUTCTime
  where
    format :: UTCTime -> ByteString
    format = fromStr . formatTime defaultTimeLocale "%a, %d %b %Y %X GMT"

    toUTCTime :: CTime -> UTCTime
    toUTCTime = posixSecondsToUTCTime . realToFrac


------------------------------------------------------------------------------
formatLogTime ctime = do
  t <- utcToLocalZonedTime $ toUTCTime ctime
  return $! format t

  where
    format :: ZonedTime -> ByteString
    format = fromStr . formatTime defaultTimeLocale "%d/%b/%Y:%H:%M:%S %z"

    toUTCTime :: CTime -> UTCTime
    toUTCTime = posixSecondsToUTCTime . realToFrac


------------------------------------------------------------------------------
parseHttpTime = return . toCTime . prs . S.unpack
  where
    parseTime =
#if MIN_VERSION_time(1,10,0)
      parseTimeM True
#else
      Time.parseTime
#endif

    prs :: String -> Maybe UTCTime
    prs = parseTime defaultTimeLocale "%a, %d %b %Y %H:%M:%S GMT"

    toCTime :: Maybe UTCTime -> CTime
    toCTime (Just t) = fromInteger $ truncate $ utcTimeToPOSIXSeconds t
    toCTime Nothing  = fromInteger 0

#else

------------------------------------------------------------------------------
formatLogTime t = do
    ptr <- mallocBytes 40
    c_format_log_time t ptr
    S.unsafePackMallocCString ptr


------------------------------------------------------------------------------
formatHttpTime t = do
    ptr <- mallocBytes 40
    c_format_http_time t ptr
    S.unsafePackMallocCString ptr


------------------------------------------------------------------------------
parseHttpTime s = S.unsafeUseAsCString s $ \ptr ->
    c_parse_http_time ptr

#endif


------------------------------------------------------------------------------
-- | Adapted from:
--
-- <https://www.iana.org/assignments/http-status-codes/http-status-codes.txt>
statusReasonMap :: IM.IntMap ByteString
statusReasonMap = IM.fromList [
        (100, "Continue"),
        (101, "Switching Protocols"),
        (102, "Processing"),
        (103, "Early Hints"),
        -- 104-199 Unassigned
        (200, "OK"),
        (201, "Created"),
        (202, "Accepted"),
        (203, "Non-Authoritative Information"),
        (204, "No Content"),
        (205, "Reset Content"),
        (206, "Partial Content"),
        (207, "Multi-Status"),
        (208, "Already Reported"),
        -- 209-225 Unassigned
        (226, "IM Used"),
        -- 227-299 Unassigned,
        (300, "Multiple Choices"),
        (301, "Moved Permanently"),
        (302, "Found"),
        (303, "See Other"),
        (304, "Not Modified"),
        (305, "Use Proxy"),
        (306, "(Unused)"),
        (307, "Temporary Redirect"),
        (308, "Permanent Redirect"),
        -- 309-399 Unassigned
        (400, "Bad Request"),
        (401, "Unauthorized"),
        (402, "Payment Required"),
        (403, "Forbidden"),
        (404, "Not Found"),
        (405, "Method Not Allowed"),
        (406, "Not Acceptable"),
        (407, "Proxy Authentication Required"),
        (408, "Request Timeout"),
        (409, "Conflict"),
        (410, "Gone"),
        (411, "Length Required"),
        (412, "Precondition Failed"),
        (413, "Payload Too Large"),
        (414, "URI Too Long"),
        (415, "Unsupported Media Type"),
        (416, "Range Not Satisfiable"),
        (417, "Expectation Failed"),
        -- 418-420 Unassigned
        (421, "Misdirected Request"),
        (422, "Unprocessable Entity"),
        (423, "Locked"),
        (424, "Failed Dependency"),
        (425, "Too Early"),
        (426, "Upgrade Required"),
        -- 427 Unassigned
        (428, "Precondition Required"),
        (429, "Too Many Requests"),
        -- 430 Unassigned
        (431, "Request Header Fields Too Large"),
        -- 432-450 Unassigned
        (451, "Unavailable For Legal Reasons"),
        -- 452-499 Unassigned
        (500, "Internal Server Error"),
        (501, "Not Implemented"),
        (502, "Bad Gateway"),
        (503, "Service Unavailable"),
        (504, "Gateway Timeout"),
        (505, "HTTP Version Not Supported"),
        (506, "Variant Also Negotiates"),
        (507, "Insufficient Storage"),
        (508, "Loop Detected"),
        -- 509 Unassigned
        (510, "Not Extended"),
        (511, "Network Authentication Required")
        -- 512-599 Unassigned
    ]

------------------------------------------------------------------------------
-- Deprecated functions

-- | See 'rqClientAddr'.
rqRemoteAddr :: Request -> ByteString
rqRemoteAddr = rqClientAddr
{-# DEPRECATED rqRemoteAddr "(snap-core >= 1.0.0.0) please use 'rqClientAddr', this will be removed in 1.1.*" #-}

-- | See 'rqClientPort'.
rqRemotePort :: Request -> Int
rqRemotePort = rqClientPort
{-# DEPRECATED rqRemotePort "(snap-core >= 1.0.0.0) please use 'rqClientPort', this will be removed in 1.1.*" #-}
