-----------------------------------------------------------------------------
-- |
-- Module      :  TinyHTTP
-- Copyright   :  (c) Don Stewart 2006
-- License     :  BSD3-style (see LICENSE)
-- 
-- Maintainer  :  dons@cse.unsw.edu.au
-- Stability   :  stable
-- Portability :  portable
--
-- Minimal HTTP functionality
--
-----------------------------------------------------------------------------

module TinyHTTP (
    Proxy, getStatus,
    module Network.URI
  ) where

import Network
import Network.URI hiding (authority)
import Control.Monad.Error 

import Data.List        (findIndex)
import Data.Maybe

import System.IO

userAgent :: String
userAgent = "urlcheck/0.1 (http://www.cse.unsw.edu.au/~dons/urlcheck)"

getStatus :: URI -> Proxy -> IO (Either String Int)
getStatus uri proxy = chase uri 5
  where
    chase _ 0 = return (Left "Too many redirects.")
    chase u n = do
        s <- getURI u proxy
        case status s of
            n | n `elem` [301,302,303,307] -> case redirect s of
                    Right u' -> chase u' (n-1)
                    Left err -> return (Left err)
            200 -> return (Right 200)
            n   -> return (Right n)

        -- Parse the HTTP response code in format: HTTP/1.1 200 Success.
    status   h = (read . (!!1) . words . (!!0)) h :: Int

    redirect h
          | Just loc <- getHeader "Location" h = case parseURI loc of
                Nothing -> do
                    let furl = fullUrl loc
                    case parseURI furl of
                        Nothing -> fail "Invalid redirect"
                        Just u  -> return u
                Just u  -> return u
          | otherwise = fail "No Location header found in 3xx response."

    fullUrl loc = case uriAuthority uri of
            Nothing -> error "No authority string."
            Just a  -> uriScheme uri ++ "//" ++ (uriRegName a) ++ loc


getHeader :: String -> [String] -> Maybe String
getHeader _   []     = Nothing
getHeader hdr (_:hs) = lookup hdr $ concatMap mkassoc hs
    where
      removeCR   = takeWhile (/='\r')
      mkassoc s  = case findIndex (==':') s of
                    Just n  -> [(take n s, removeCR $ drop (n+2) s)]
                    Nothing -> []

getURI :: URI -> Proxy -> IO [String]
getURI uri proxy = readNBytes 1024 proxy uri request ""
    where
      request  | Nothing <- proxy =
                   ["HEAD " ++ absPath ++ " HTTP/1.1",
                   "Host: " ++ host,
                   "User-Agent: " ++ userAgent,
                   "Connection: close", ""]
               | otherwise       =
                   ["HEAD " ++ show uri ++ " HTTP/1.0", ""]

      absPath = case uriPath uri ++ uriQuery uri ++ uriFragment uri of
                   url@('/':_) -> url
                   url         -> '/':url

      host = case uriAuthority uri of
                Nothing -> error "getURI: No authority string."
                Just u  -> uriRegName u

readNBytes :: Int -> Proxy -> URI -> [String] -> String -> IO [String]
readNBytes n proxy uri headers body = withSocketsDo $ do
    h <- connectTo host (PortNumber (fromInteger port))
    mapM_ (\s -> hPutStr h (s ++ "\r\n")) headers
    hPutStr h body
    hFlush h
    s <- lines `fmap` hGetN n h
    hClose h
    return s
  where
    (host, port) = fromMaybe (authority uri, 80) proxy

    hGetN :: Int -> Handle -> IO String
    hGetN i h | i `seq` h `seq` False = undefined -- strictify
    hGetN 0 _ = return []
    hGetN i h = do eof <- hIsEOF h
                   if eof then return []
                          else liftM2 (:) (hGetChar h) (hGetN (i-1) h)

type Proxy = Maybe (String, Integer)

authority :: URI -> String
authority = uriRegName . maybe (error "authority") id . uriAuthority
