{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

module Web.Welshy
    ( welshy, welshyApp

      -- * Middleware & Routing
    , Welshy, middleware, route, RoutePattern
    , get, post, put, patch, delete, head, options

      -- * Actions
    , Action
    , halt, pass
    , catchIO

      -- ** Request
    , request, body
    , capture, captures
    , queryParam, maybeQueryParam, queryParams
    , jsonParam, maybeJsonParam, jsonParams, jsonData
    , bearerAuth

      -- ** Response
    , status, header
    , text, text'
    , html, html'
    , json
    , file, filePart
    , source

      -- * Parameter Parsing
    , Param
    , FromText(..), maybeFromText
    ) where

import Control.Applicative
import Control.Exception
import qualified Control.Exception.Lifted as Lifted
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Writer hiding (pass)
import Data.Monoid
import Data.Text (Text)
import qualified Data.Text as T
import Network.HTTP.Types
import Network.Wai
import Network.Wai.Handler.Warp
import System.IO

import Prelude hiding (head)

import Web.Welshy.Action
import Web.Welshy.FromText
import Web.Welshy.Request
import Web.Welshy.Response

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

-- Note [Exception handling]
-- ~~~~~~~~~~~~~~~~~~~~~~~~~
-- Ideally, all exceptions would be caught by the server and a 500 response
-- sent to the client. Alas, due to lazyness this is not possible: any
-- exceptions occuring inside a 'ResponseBuilder' would need to be caught on
-- a higher level. ('Warp' provides 'settingsOnException', but at that point
-- the connection has already been dropped and we can't send a response to
-- the client anymore.)
--
-- Effectively, this means that if something like the following happens
-- the client will simply get an empty reply:
--
-- > text $ error "wat"
--
-- (The exception will be logged to stderr though.)

-- TODO: clarify this in user-visible documentation.

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

-- | We use this monad to compose WAI 'Middleware', using the 'middleware'
-- and 'route' functions.
newtype Welshy a = Welshy (Writer [Middleware] a)
    deriving (Functor, Applicative, Monad)

-- | Run a Welshy app using the Warp server.
welshy :: Port -> Welshy () -> IO ()
welshy p w = do
    putStr "Aye, Dwi iawn 'n feddw!"
    putStrLn $ " (port " ++ show p ++ ") (ctrl-c to quit)"
    run p (welshyApp w)

-- | Turns a Welshy app into a WAI 'Application'.
welshyApp :: Welshy () -> Application
welshyApp (Welshy w) = foldr id notFound (catchError : execWriter w)
    where
        notFound :: Application
        notFound _ = return $ ResponseBuilder notFound404 [] mempty

        -- see Note [Exception Handling]
        catchError :: Middleware
        catchError app req = Lifted.catch (app req) $ \e -> do
            liftIO $ hPrint stderr (e :: SomeException)
            return $ ResponseBuilder status500 [] mempty

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

-- | Insert middleware into the app. Note that unlike in Scotty,
-- each middleware is run at the point of insertion.
middleware :: Middleware -> Welshy ()
middleware = Welshy . tell . pure

get :: RoutePattern -> Action () -> Welshy ()
get = route GET

post :: RoutePattern -> Action () -> Welshy ()
post = route POST

put :: RoutePattern -> Action () -> Welshy ()
put = route PUT

patch :: RoutePattern -> Action () -> Welshy ()
patch = route PATCH

delete :: RoutePattern -> Action () -> Welshy ()
delete = route DELETE

head :: RoutePattern -> Action () -> Welshy ()
head = route HEAD

options :: RoutePattern -> Action () -> Welshy ()
options = route OPTIONS

-- | Sinatra-style route pattern. Named parameters are prepended with
-- a colon (e.g. @\"\/users\/:id\"@) and can be accessed with 'capture'.
type RoutePattern = Text

-- | Define a route for an HTTP method and URL pattern that runs the given
-- action. Routes are matched in the order they are defined. If no route
-- matches, a 404 response is returned.
route :: StdMethod -> RoutePattern -> Action () -> Welshy ()
route met pat act = middleware $ \nextApp req ->
    case matchRoute met pat req of
        Nothing   -> nextApp req
        Just caps -> execAction act caps nextApp req

matchRoute :: StdMethod -> RoutePattern -> Request -> Maybe [Param]
matchRoute met pat req =
    if Right met == parseMethod (requestMethod req)
        then go (filter (/= T.empty) $ T.split (=='/') pat) (pathInfo req) []
        else Nothing
    where
        go []     []     prs  = Just prs
        go []     _      _    = Nothing
        go _      []     _    = Nothing
        go (p:ps) (r:rs) prs
            | p == r          = go ps rs prs
            | T.null p        = Nothing
            | T.head p == ':' = go ps rs $ (T.tail p, r) : prs
            | otherwise       = Nothing