{-|
  A library for HTTP server.
-}
module Network.Web.Server (connection, WebServer, WebConfig(..)) where

import Control.Exception
import Control.Monad
import Control.Applicative
import Data.Char
import Data.Maybe
import Data.Time
import IO
import Network.Web.HTTP hiding (receive,respond)
import qualified Network.Web.HTTP as HTTP (receive,respond)
import Network.Web.Date
import Network.Web.Params
import System.Timeout

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

{-|
  The type for HTTP server.
-}
type WebServer = Maybe Request -> IO Response

{-|
  The configuration for 'connection'.
-}
data WebConfig = WebConfig {
    -- | A hook to be called when an HTTP connection is closed.
    closedHook :: String -> IO ()
    -- | A hook to be called when access succeeds.
  , accessHook :: String -> IO ()
    -- | A hook to be called when an access error occurs.
  , errorHook :: String -> IO ()
    -- | A hook to be called when a fatal error occurs.
  , fatalErrorHook :: String -> IO ()
    -- | A time to unblock receiving an HTTP request in seconds.
  , connectionTimer :: Int
}

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

{-|
  A function to run an 'WebServer'. 'Handle' should be mode by
  converting an accepted socket.
  Keep-alive / termination of HTTP 1.0 and HTTP 1.1 is correctly handled.
  So, 'WebServer' need not to handle the Connection: header in response.
  The Date: header is automatically added in response.
-}
connection :: Handle -> WebServer -> WebConfig -> IO ()
connection hdl srv cnf = session hdl srv cnf `catches` serverError
  where
    serverError =
      [Handler (\e -> closedHook cnf (show (e::ServerException))),
       Handler (\e -> errorHook  cnf (show (e::SomeException)))]

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

session :: Handle -> WebServer -> WebConfig -> IO ()
session hdl svr cnf = do
    mreq <- recvRequest hdl cnf
    rsp <- runServer svr mreq
    persist <- sendResponse hdl cnf rsp mreq
    case persist of
      Close -> closedHook cnf $ "Connection is closed"
      Keep  -> session hdl svr cnf
      _     -> return () -- never reached
  where
    runServer server mreq = do
        date <- utcToDate <$> getCurrentTime
        addDate date <$> server mreq
    addDate date rsp  = insertField FkDate date rsp

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

recvRequest :: Handle -> WebConfig -> IO (Maybe Request)
recvRequest hdl cnf = do
    mmreq <- timeout tm (HTTP.receive hdl)
    case mmreq of
      Nothing   -> throw TimeOut
      Just mreq -> return mreq
 where
   microseconds = 1000000
   tm = connectionTimer cnf * microseconds

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

sendResponse :: Handle -> WebConfig -> Response -> Maybe Request -> IO Persist
sendResponse hdl cnf rsp (Just req) = do
    let ver = reqVersion req
        cnct = lookupField FkConnection req
        status = rspStatus rsp
        persist = if badStatus status
                  then Close
                  else checkPersist ver cnct rsp
        hook = accessHook cnf
    sendResponse' hdl ver persist rsp hook
sendResponse hdl cnf rsp Nothing = do
    let hook = errorHook cnf
    sendResponse' hdl HTTP10 Close rsp hook

sendResponse' :: Handle -> Version -> Persist -> Response -> (String -> IO ()) -> IO Persist
sendResponse' hdl ver persist rsp hook = do
    HTTP.respond hdl ver persist rsp
    hook $ rspLogMsg rsp ++ " [" ++ show (rspStatus rsp) ++ "]"
    return persist

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

-- CGI and HTTP/1.0 -> Close

checkPersist :: Version -> Maybe String -> Response -> Persist
checkPersist HTTP11 Nothing     _   = Keep
checkPersist HTTP11 (Just cnct) _
    | read cnct == Close            = Close
    | otherwise                     = Keep
checkPersist HTTP10 Nothing     _   = Close
checkPersist HTTP10 (Just cnct) rsp
    | read cnct == Keep        = if isJust (rspBody rsp) &&
                                    isNothing (rspLength rsp)
                                 then Close
                                 else Keep
    | otherwise                     = Close