{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

module Web.Welshy.Action where

import Control.Applicative
import Control.Arrow
import Control.Exception
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import Control.Monad.Trans.Resource
import qualified Data.Aeson as A
import qualified Data.ByteString.Lazy as BL
import Data.Conduit.Lazy
import Data.Maybe
import Data.Monoid
import Data.Text (Text)
import Network.HTTP.Types
import Network.Wai

-----------------------------------------------------------------------

data Result a = Ok a Response | Halt (Action ()) | Pass

newtype Action a = Action { runAction :: Env -> Response -> IO (Result a) }

instance Functor Action where
    fmap f m = Action $ \r s -> runAction m r s >>= \case
        Ok a s1 -> return $ Ok (f a) s1
        Halt m1 -> return $ Halt m1
        Pass    -> return $ Pass

instance Applicative Action where
    pure = return
    (<*>) = ap

-- TODO: this instance should be highlighted in the documentation
instance Alternative Action where
    empty = mzero
    (<|>) = mplus

instance Monad Action where
    return a = Action $ \_ s -> return $ Ok a s
    m >>= k  = Action $ \r s -> runAction m r s >>= \case
        Ok a s1 -> runAction (k a) r s1
        Halt s1 -> return $ Halt s1
        Pass    -> return $ Pass

    fail msg = halt $ error msg

instance MonadPlus Action where
    mzero       = fail "mzero"
    m `mplus` n = Action $ \r s -> runAction m r s >>= \case
        Ok a s1 -> return $ Ok a s1
        Halt __ -> runAction n r s
        Pass    -> runAction n r s

instance MonadIO Action where
    liftIO m = Action $ \_ s -> do
        a <- m
        return $ Ok a s

-- | Like `catch` but with the exception handler and result in 'Action'.
catchIO :: Exception e => IO a -> (e -> Action a) -> Action a
catchIO act h = either h return =<< (liftIO $ try $ act)

-----------------------------------------------------------------------

-- | A route, query or form parameter and its value.
type Param = (Text, Text)

data Env = Env { _captures    :: [Param]
               , _queryParams :: [Param]
               , _body        :: BL.ByteString
               , _jsonParams  :: Maybe A.Object
               , _request     :: Request }

mkEnv :: [Param] -> Request -> ResourceT IO Env
mkEnv _captures _request = do
    _body <- BL.fromChunks <$> lazyConsume (requestBody _request)
    let _queryParams = queryText _request ++ formParams _body _request
        _jsonParams = either (const Nothing) Just (A.eitherDecode _body)
    return Env {..}

queryText :: Request -> [Param]
queryText = map (second $ fromMaybe "") . queryToQueryText . queryString

formParams :: BL.ByteString -> Request -> [Param]
formParams b req =
    case lookup hContentType (requestHeaders req) of
        Just "application/x-www-form-urlencoded" ->
            map (second $ fromMaybe "") $ queryToQueryText $
            parseQuery $ BL.toStrict $ b
        _ -> []


execAction :: Action () -> [Param] -> Middleware
execAction act0 caps nextApp req = run act0 =<< mkEnv caps req
    where
        run :: Action () -> Env -> ResourceT IO Response
        run act env = (lift $ runAction act env okRes) >>= \case
            Ok _ res  -> return res
            Halt act' -> run act' env
            Pass      -> nextApp req

        okRes :: Response
        okRes = ResponseBuilder ok200 [] mempty

-----------------------------------------------------------------------

-- | Stop running the current action and continue with another one.
-- The other action will live in the same request environment and can access
-- the same route captures, but it will start with a fresh default response.
--
-- This is incredibly useful for error handling. For example:
--
-- > patch "/users/:uid" $ do
-- >     uid <- capture "uid"
-- >     user <- getUserFromDB uid
-- >             `catchIO` (\_ -> halt $ status notFound404)
-- >     ...
halt :: Action () -> Action a
halt m = Action $ \_ _ -> return $ Halt m

-- | Stop the current action and continue with the next matching route.
pass :: Action a
pass = Action $ \_ _ -> return Pass

-- | Get the raw WAI 'Request'.
request :: Action Request
request = Action $ \r s -> return $ Ok (_request r) s

-- | Get all query parameters.
queryParams :: Action [Param]
queryParams = Action $ \r s -> return $ Ok (_queryParams r) s

-- | Get all route captures.
captures :: Action [Param]
captures = Action $ \r s -> return $ Ok (_captures r) s

-- | Get the request body.
body :: Action BL.ByteString
body = Action $ \r s -> return $ Ok (_body r) s

-- | Modify the raw WAI 'Response'.
modifyResponse :: (Response -> Response) -> Action ()
modifyResponse f = Action $ \_ s -> return $ Ok () (f s)