-- -----------------------------------------------------------------------------
-- Copyright 2002, Simon Marlow.
-- Copyright 2006, Bjorn Bringert.
-- Copyright 2009, Henning Thielemann.
-- All rights reserved.
--
-- Redistribution and use in source and binary forms, with or without
-- modification, are permitted provided that the following conditions are
-- met:
--
--  * Redistributions of source code must retain the above copyright notice,
--    this list of conditions and the following disclaimer.
--
--  * Redistributions in binary form must reproduce the above copyright
--    notice, this list of conditions and the following disclaimer in the
--    documentation and/or other materials provided with the distribution.
--
--  * Neither the name of the copyright holder(s) nor the names of
--    contributors may be used to endorse or promote products derived from
--    this software without specific prior written permission.
--
-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-- -----------------------------------------------------------------------------

module Network.MoHWS.HTTP.Request (
      T(Cons), command, uri, httpVersion, headers, body,
      toHTTPbis, fromHTTPbis,
      Command, HTTP.RequestMethod(..),
      Connection(..),
      Expect(..),
      pHeaders,
      getHost,
      getConnection,
   ) where

import Text.ParserCombinators.Parsec (Parser, skipMany1, many, noneOf, )
import Network.MoHWS.ParserUtility (pCRLF, pSP, pToken, parseList, )

import qualified Network.MoHWS.HTTP.Header as Header
import qualified Network.MoHWS.HTTP.Version as HTTPVersion
import Network.MoHWS.HTTP.Header (HasHeaders, )
import Network.MoHWS.Utility (readM, )

import qualified Network.HTTP.Base as HTTP
import qualified Network.HTTP.Headers
   -- make getHeaders visible for instance declaration

import Network.Socket (HostName, )
import Network.URI (URI, nullURI, uriPath, uriQuery, )

import qualified Data.Map as Map
import Data.Monoid (Monoid, mempty, )
import Data.Char (toLower, )


-----------------------------------------------------------------------------
-- Requests

-- Request-Line   = Method SP Request-URI SP HTTP-Version CRLF

type Command = HTTP.RequestMethod

data T body =
   Cons {
      command     :: Command,
      uri         :: URI,
      httpVersion :: HTTPVersion.T,
      headers     :: Header.Group,
      body        :: body
   }


toHTTPbis :: T body -> HTTP.Request body
toHTTPbis req =
   HTTP.Request {
      HTTP.rqURI     = uri req,
      HTTP.rqMethod  = command req,
      HTTP.rqHeaders = Header.ungroup $ headers req,
      HTTP.rqBody    = body req
   }

fromHTTPbis :: HTTP.Request body -> T body
fromHTTPbis req =
   Cons {
      command     = HTTP.rqMethod req,
      uri         = HTTP.rqURI req,
      httpVersion = HTTPVersion.http1_1,
      headers     = Header.group $ HTTP.rqHeaders req,
      body        = HTTP.rqBody req
   }


instance Show (T body) where
   showsPrec _ Cons{command = cmd, uri = loc, httpVersion = ver} =
      shows cmd . (' ':) . shows loc . (' ':) . shows ver

instance HasHeaders (T body) where
   getHeaders = Header.ungroup . headers
   setHeaders req hs = req { headers = Header.group hs}

instance Functor T where
   fmap f req =
      Cons {
         command     = command     req,
         uri         = uri         req,
         httpVersion = httpVersion req,
         headers     = headers     req,
         body        = f $ body req
      }



-- Request parsing

-- Parse the request line and the headers, but not the body.
pHeaders :: Monoid body => Parser (T body)
pHeaders =
   do (cmd,loc,ver) <- pCommandLine
      hdrs <- Header.pGroup
      _ <- pCRLF
      return $ Cons cmd loc ver hdrs mempty

pCommandLine :: Parser (Command, URI, HTTPVersion.T)
pCommandLine =
   do cmd <- pCommand
      skipMany1 pSP
      loc <- pURI
      skipMany1 pSP
      ver <- HTTPVersion.pInRequest
      _ <- pCRLF
      return (cmd,loc,ver)

commandDictionary :: Map.Map String Command
commandDictionary =
   Map.fromList $
   ("HEAD",    HTTP.HEAD)    :
   ("PUT",     HTTP.PUT)     :
   ("GET",     HTTP.GET)     :
   ("POST",    HTTP.POST)    :
   ("DELETE",  HTTP.DELETE)  :
   ("OPTIONS", HTTP.OPTIONS) :
   ("TRACE",   HTTP.TRACE)   :
--   ("CONNECT", HTTP.CONNECT) :
   []

pCommand :: Parser Command
pCommand =
   fmap (\tok -> Map.findWithDefault (HTTP.Custom tok) tok commandDictionary) $
   pToken

pURI :: Parser URI
pURI =
   do u <- many (noneOf [' '])
      -- FIXME: this does not handle authority Request-URIs
      -- maybe (fail "Bad Request-URI") return $ parseURIReference u
      return $ laxParseURIReference u

-- also accepts characters [ ] " in queries, which is sometimes quite handy
laxParseURIReference :: String -> URI
laxParseURIReference u =
   let (p,q) = break ('?'==) u
   in  nullURI{uriPath=p, uriQuery=q}

-----------------------------------------------------------------------------
-- Getting specific request headers


data Connection =
     ConnectionClose
   | ConnectionKeepAlive -- non-std?  Netscape generates it.
   | ConnectionOther String
   deriving (Eq, Show)

parseConnection :: String -> [Connection]
parseConnection =
   let fn "close"      = ConnectionClose
       fn "keep-alive" = ConnectionKeepAlive
       fn other        = ConnectionOther other
   in  map (fn . map toLower) . parseList

getConnection :: HasHeaders a => a -> [Connection]
getConnection =
   concatMap parseConnection . Header.lookupMany Header.HdrConnection

data Expect = ExpectContinue
  deriving Show

-- parseExpect :: String -> Maybe Expect
-- parseExpect s =
--   case parseList s of
--      ["100-continue"] -> Just ExpectContinue
--      _                -> Nothing


getHost :: HasHeaders a => a -> Maybe (HostName, Maybe Int)
getHost x = Header.lookup Header.HdrHost x >>= parseHost

parseHost :: String -> Maybe (HostName, Maybe Int)
parseHost s =
   let (host,prt) = break (==':') s
   in  case prt of
          ""       -> Just (host, Nothing)
          ':':port -> readM port >>= \p -> Just (host, Just p)
          _        -> Nothing