{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE PackageImports #-}
---------------------------------------------------------
-- |
-- Module        : Network.HTTP.Wget
-- Copyright     : Michael Snoyman
-- License       : BSD3
--
-- Maintainer    : Michael Snoyman <michael@snoyman.com>
-- Stability     : Stable
-- Portability   : portable
--
-- Provide a simple HTTP client interface by wrapping the wget command line tool.
--
---------------------------------------------------------
module Network.HTTP.Wget
    ( wget
    , wget'
    , WgetException (..)
    ) where

import System.Process
import System.Exit
import System.IO
import Numeric (showHex)
import Data.List (intercalate)
import "transformers" Control.Monad.IO.Class
import Control.Failure
import Control.Exception
import Data.Generics
import Data.Char (isSpace)
import Control.Arrow (first, second)

newtype WgetException = WgetException String
    deriving (Show, Typeable)
instance Exception WgetException

-- | Get a response from the given URL with the given parameters.
wget :: (MonadIO m, MonadFailure WgetException m)
     => String -- ^ The URL.
     -> [(String, String)] -- ^ Get parameters.
     -> [(String, String)] -- ^ Post parameters. If empty, this will be a get request.
     -> m String -- ^ The response body.
wget url get post = snd `fmap` wget' url get post

-- | Get a response from the given URL with the given parameters, including headers.
wget' :: (MonadIO m, MonadFailure WgetException m)
     => String -- ^ The URL.
     -> [(String, String)] -- ^ Get parameters.
     -> [(String, String)] -- ^ Post parameters. If empty, this will be a get request.
     -> m ([(String, String)], String) -- ^ The headers and response body.
wget' url get post = do
    let getSepChar :: Char
        getSepChar = if '?' `elem` url then '&' else '?'
        get' :: String
        get' = if null get then "" else getSepChar : urlEncodePairs get
        post' :: [String]
        post' = if null post
                    then []
                    else ["--post-data", urlEncodePairs post]
    (Nothing, Just hout, Just herr, phandle) <- liftIO $
        createProcess $ (proc "wget"
            ((url ++ get') : post' ++ ["-O", "-", "--save-headers"])
        ) { std_out = CreatePipe, std_err = CreatePipe }
    exitCode <- liftIO $ waitForProcess phandle
    case exitCode of
        ExitSuccess -> liftIO $ parseHeaders `fmap` hGetContents hout
        _ -> liftIO (hGetContents herr) >>= failure . WgetException

parseHeaders :: String -> ([(String, String)], String)
parseHeaders = first (parseHeaders' . drop 1 . lines) . breakDoubleNewLine

breakDoubleNewLine :: String -> (String, String)
breakDoubleNewLine = first ($ "") . h where
    h :: String -> (String -> String, String)
    h ('\r':'\n':x) = h $ '\n' : x
    h ('\n':'\r':'\n':x) = h $ '\n':'\n':x
    h ('\n':'\n':x) = (id, x)
    h (c:x) =
        let (a, b) = h x
         in ((:) c . a, b)
    h [] = (id, "") -- though this should never happen

parseHeaders' :: [String] -> [(String, String)]
parseHeaders' = map helper where
    helper = second (dropWhile isSpace . dropColon) . break (== ':')
    dropColon (':':x) = x
    dropColon x = x

urlEncodePairs :: [(String, String)] -> String
urlEncodePairs = intercalate "&" . map urlEncodePair

urlEncodePair :: (String, String) -> String
urlEncodePair (x, y) = urlEncode x ++ '=' : urlEncode y

urlEncode :: String -> String
urlEncode = concatMap urlEncodeChar

urlEncodeChar :: Char -> String
urlEncodeChar x
    | safeChar (fromEnum x) = return x
    | otherwise = '%' : showHex (fromEnum x) ""

safeChar :: Int -> Bool
safeChar x
    | x >= fromEnum 'a' && x <= fromEnum 'z' = True
    | x >= fromEnum 'A' && x <= fromEnum 'Z' = True
    | x >= fromEnum '0' && x <= fromEnum '9' = True
    | otherwise = False