module Network.HTTP.Types
(
  -- * Case insensitive HTTP ByteStrings
  HttpCIByteString(..)
, mkHttpCIByteString
  -- * Methods
, Method
, methodGet
, methodPost
, methodHead
, methodPut
, methodDelete
, methodTrace
, methodConnect
, methodOptions
, MethodADT(GET, POST, HEAD, PUT, DELETE, TRACE, CONNECT, OPTIONS)
, methodToADT
, methodFromADT
, stringToMethodADT
, methodADTToString
  -- * Versions
, HttpVersion(httpMajor, httpMinor)
, http09
, http10
, http11
  -- * Status
, Status(statusCode, statusMessage)
, status200, statusOK
, status201, statusCreated
, status301, statusMovedPermanently
, status302, statusFound
, status303, statusSeeOther
, status400, statusBadRequest
, status401, statusUnauthorized
, status403, statusForbidden
, status404, statusNotFound
, status405, statusNotAllowed
, status500, statusServerError
  -- * Headers
, RequestHeaders
, ResponseHeaders
  -- * Query string
, Query
, QuerySimple
)
where

import           Data.Char
import           Data.Maybe
import           Data.String
import qualified Data.ByteString       as B
import qualified Data.ByteString.Char8 as Ascii

localError :: String -> String -> a
localError f s = error $ "Network.HTTP.Types." ++ f ++ ": " ++ s

-- | Case-insensitive HTTP ByteStrings, mostly for use in Header names.
data HttpCIByteString
    = HttpCIByteString {
        ciOriginal :: !B.ByteString
      , ciLowerCase :: !B.ByteString
      }

mkHttpCIByteString :: B.ByteString -> HttpCIByteString
mkHttpCIByteString orig = HttpCIByteString {
                            ciOriginal = orig
                          , ciLowerCase = Ascii.map toLower orig
                          }

instance Eq HttpCIByteString where
    HttpCIByteString { ciLowerCase = a } == HttpCIByteString { ciLowerCase = b } 
        = a == b

instance Ord HttpCIByteString where
    compare HttpCIByteString { ciLowerCase = a } HttpCIByteString { ciLowerCase = b } 
        = compare a b

instance Show HttpCIByteString where
    show = show . ciOriginal

instance IsString HttpCIByteString where
    fromString = mkHttpCIByteString . Ascii.pack

-- | HTTP method (flat string type).
type Method = B.ByteString

-- | HTTP Method constants.
methodGet, methodPost, methodHead, methodPut, methodDelete, methodTrace, methodConnect, methodOptions :: Method
methodGet     = Ascii.pack "GET"
methodPost    = Ascii.pack "POST"
methodHead    = Ascii.pack "HEAD"
methodPut     = Ascii.pack "PUT"
methodDelete  = Ascii.pack "DELETE"
methodTrace   = Ascii.pack "TRACE"
methodConnect = Ascii.pack "CONNECT"
methodOptions = Ascii.pack "OPTIONS"

-- | HTTP method (ADT version).
-- 
-- Note that the Show instance is only for debugging and should NOT be used to generate HTTP method strings; use 'methodToByteString' instead.
-- 
-- The constructor 'OtherMethod' is not exported for forwards compatibility reasons.
data MethodADT
    = GET
    | POST
    | HEAD  
    | PUT
    | DELETE
    | TRACE
    | CONNECT
    | OPTIONS
    | OtherMethod B.ByteString
    deriving (Show, Eq, Ord)

-- These are ordered by suspected frequency. More popular methods should go first.
-- The reason is that methodListA and methodListB are used with lookup.
-- lookup is probably faster for these few cases than setting up an elaborate data structure.
methodListA :: [(Method, MethodADT)]
methodListA 
    = [ (methodGet, GET)
      , (methodPost, POST)
      , (methodHead, HEAD)
      , (methodPut, PUT)
      , (methodDelete, DELETE)
      , (methodTrace, TRACE)
      , (methodConnect, CONNECT)
      , (methodOptions, OPTIONS)
      ]

methodListB :: [(MethodADT, Method)]
methodListB = map (\(a, b) -> (b, a)) methodListA

-- | Convert a method 'ByteString' to a 'MethodADT'.
methodToADT :: Method -> MethodADT
methodToADT bs = fromMaybe (OtherMethod bs) $ lookup bs methodListA

-- | Convert a 'MethodADT' to a 'ByteString'.
methodFromADT :: MethodADT -> Method
methodFromADT m
    = case m of
        OtherMethod bs -> bs
        _ -> fromMaybe (localError "methodToByteString" "This should not happen (methodListB is incomplete)") $
             lookup m methodListB

-- | Convert a method 'String' to a 'MethodADT'.
stringToMethodADT :: String -> MethodADT
stringToMethodADT = methodToADT . Ascii.pack

-- | Convert a 'MethodADT' to a 'String'.
methodADTToString :: MethodADT -> String
methodADTToString = Ascii.unpack . methodFromADT

-- | HTTP Version.
-- 
-- Note that the Show instance is intended merely for debugging.
data HttpVersion 
    = HttpVersion {
        httpMajor :: !Int 
      , httpMinor :: !Int
      }
    deriving (Eq, Ord)

instance Show HttpVersion where
    show (HttpVersion major minor) = "HTTP/" ++ show major ++ "." ++ show minor

-- | HTTP 0.9
http09 :: HttpVersion
http09 = HttpVersion 0 9

-- | HTTP 1.0
http10 :: HttpVersion
http10 = HttpVersion 1 0

-- | HTTP 1.1
http11 :: HttpVersion
http11 = HttpVersion 1 1

-- | HTTP Status.
-- 
-- Only the 'statusCode' is used for comparisons.
-- 
-- Note that the Show instance is only for debugging.
data Status
    = Status {
        statusCode :: Int
      , statusMessage :: B.ByteString
      }
    deriving (Show)

instance Eq Status where
    Status { statusCode = a } == Status { statusCode = b } = a == b

instance Ord Status where
    compare Status { statusCode = a } Status { statusCode = b } = a `compare` b

-- | OK
status200, statusOK :: Status
status200 = Status 200 $ Ascii.pack "OK"
statusOK = status200

-- | Created
status201, statusCreated :: Status
status201 = Status 200 $ Ascii.pack "Created"
statusCreated = status201

-- | Moved Permanently
status301, statusMovedPermanently :: Status
status301 = Status 301 $ Ascii.pack "Moved Permanently"
statusMovedPermanently = status301

-- | Found
status302, statusFound :: Status
status302 = Status 302 $ Ascii.pack "Found"
statusFound = status302

-- | See Other
status303, statusSeeOther :: Status
status303 = Status 303 $ Ascii.pack "See Other"
statusSeeOther = status303

-- | Bad Request
status400, statusBadRequest :: Status
status400 = Status 400 $ Ascii.pack "Bad Request"
statusBadRequest = status400

-- | Unauthorized
status401, statusUnauthorized :: Status
status401 = Status 401 $ Ascii.pack "Unauthorized"
statusUnauthorized = status401

-- | Forbidden
status403, statusForbidden :: Status
status403 = Status 403 $ Ascii.pack "Forbidden"
statusForbidden = status403

-- | Not Found
status404, statusNotFound :: Status
status404 = Status 404 $ Ascii.pack "Not Found"
statusNotFound = status404

-- | Method Not Allowed
status405, statusNotAllowed :: Status
status405 = Status 405 $ Ascii.pack "Method Not Allowed"
statusNotAllowed = status405

-- | Internal Server Error
status500, statusServerError :: Status
status500 = Status 500 $ Ascii.pack "Internal Server Error"
statusServerError = status500

-- | Request Header
type RequestHeaders = [(HttpCIByteString, B.ByteString)]

-- | Response Headers
type ResponseHeaders = [(HttpCIByteString, B.ByteString)]

-- | Query.
-- 
-- General form: a=b&c=d, but if the value is Nothing, it becomes
-- a&c=d.
type Query = [(B.ByteString, Maybe B.ByteString)]

-- | Simplified Query type without support for parameter-less items.
type QuerySimple = [(B.ByteString, B.ByteString)]