{-# LANGUAGE FlexibleInstances #-}
module Network.Shelob.Client where

import           Control.Monad.IO.Class (MonadIO(..))
import qualified Control.Exception as Ex
import qualified Data.Aeson as Aeson
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS8
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Network.Socket as Sock
import qualified System.IO as H

import           Network.Shelob
import           Network.Shelob.Connection
import           Network.Shelob.Types


newtype ConnM a = ConnM { runConnM :: Connection -> IO a }

instance Functor ConnM where
  fmap f (ConnM g) = ConnM (\ c -> fmap f (g c))

instance Applicative ConnM where
  pure x = ConnM (\ _ -> return x)
  ConnM f <*> ConnM x = ConnM $ \ c -> do
    f' <- f c
    x' <- x c
    return (f' x')

instance Monad ConnM where
  ConnM x >>= f = ConnM $ \ c -> do
    x' <- x c
    runConnM (f x') c

instance MonadIO ConnM where
  liftIO m = ConnM (\ _ -> m)

-- * HTTPPayload

-- | Types that implement 'HTTPPayload' can be transparently used as
-- the body of an HTTP request.
class HTTPPayload t where
  toPayload :: t -> BS.ByteString

instance HTTPPayload BS.ByteString where
  toPayload = id

instance HTTPPayload T.Text where
  toPayload = T.encodeUtf8

-- | The 'JSONPayload' type is used so that we can interpret the
-- payload of a response
newtype JSONPayload t = JSONPayload { fromJSONPayload :: t }

instance Aeson.FromJSON t => Aeson.FromJSON (JSONPayload t) where
  parseJSON x = fmap JSONPayload (Aeson.parseJSON x)

-- * HTTPResult
class HTTPResult t where
  fromResponse :: Either HTTPError Response -> t

instance HTTPResult r => HTTPResult (Either HTTPError r) where
  fromResponse (Left err) = Left err
  fromResponse (Right v)  = Right (fromResponse (Right v))

instance HTTPResult r => HTTPResult (Maybe r) where
  fromResponse (Left _)  = Nothing
  fromResponse (Right v) = Just (fromResponse (Right v))

onSuccess :: (Response -> a) -> Either HTTPError Response -> a
onSuccess _ (Left err) = Ex.throw err
onSuccess f (Right v)  = f v

instance HTTPResult Response where
  fromResponse = onSuccess id

instance HTTPResult BS.ByteString where
  fromResponse = onSuccess (go . responseBody)
    where go Nothing  = Ex.throw HTTPError
          go (Just x) = x

instance Aeson.FromJSON t => HTTPResult (JSONPayload t) where
  fromResponse = onSuccess (go . responseBody)
    where go Nothing  = Ex.throw HTTPError
          go (Just x)
            | Just rs <- Aeson.decodeStrict x = JSONPayload rs
            | otherwise = Ex.throw HTTPError

withConn :: Connection -> ConnM a -> IO a
withConn conn m = do
  rs <- runConnM m conn
  connClose conn
  return rs

doRequest :: HTTPResult r => Request -> ConnM r
doRequest req = ConnM (\ c -> fromResponse `fmap` makeRequest c req)

head :: HTTPResult r => BS.ByteString -> [Header] -> ConnM r
head p hs = doRequest (Request MHead p (hs' ++ hs) "")
  where hs' = [ Header "Content-Length" "0"
              , userAgent
              ]

get :: HTTPResult r => BS.ByteString -> [Header] -> ConnM r
get p hs = doRequest (Request MGet p (hs' ++ hs) "")
  where hs' = [ Header "Content-Length" "0"
              , userAgent
              ]

post :: HTTPResult r => BS.ByteString -> [Header] -> BS.ByteString -> ConnM r
post p hs body = doRequest (Request MPost p (hs' ++ hs) body)
  where hs' = [ Header "Content-Length" (BS8.pack (show (BS.length body)))
              , userAgent
              ]

httpConnection :: String -> IO Connection
httpConnection host = do
  addrInfo <- Sock.getAddrInfo Nothing (Just host) (Just "http")
  case addrInfo of
    [] -> error ("unable to look up " ++ host)
    info:_ -> do
      s <- Sock.socket (Sock.addrFamily info)
                       (Sock.addrSocketType info)
                       (Sock.addrProtocol info)
      Sock.connect s (Sock.addrAddress info)
      h <- Sock.socketToHandle s H.ReadWriteMode
      return $ handleToConnection h
