module Web.Scotty
    ( 
      scotty, scottyApp
      
      
      
      
      
    , middleware, get, post, put, delete, addroute
      
      
    , request, param, jsonData
      
    , status, header, redirect
      
      
      
      
    , text, html, file, json
      
    , raise, rescue, next
      
    , ScottyM, ActionM, Parsable
    ) where
import Blaze.ByteString.Builder (fromByteString, fromLazyByteString)
import Control.Applicative
import Control.Monad.Error
import Control.Monad.Reader
import qualified Control.Monad.State as MS
import Control.Monad.Trans.Resource (ResourceT)
import qualified Data.Aeson as A
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.CaseInsensitive as CI
import Data.Default (Default, def)
import Data.Conduit.Lazy (lazyConsume)
import Data.Maybe (fromMaybe)
import Data.Monoid (mconcat)
import qualified Data.Text.Lazy as T
import Data.Text.Lazy.Encoding (encodeUtf8)
import Network.HTTP.Types
import Network.Wai
import Network.Wai.Handler.Warp (Port, run)
import Web.Scotty.Util
data ScottyState = ScottyState { middlewares :: [Middleware]
                               , routes :: [Middleware]
                               }
instance Default ScottyState where
    def = ScottyState [] []
newtype ScottyM a = S { runS :: MS.StateT ScottyState IO a }
    deriving (Monad, MonadIO, Functor, MS.MonadState ScottyState)
scotty :: Port -> ScottyM () -> IO ()
scotty p s = putStrLn "Setting phasers to stun... (ctrl-c to quit)" >> (run p =<< scottyApp s)
scottyApp :: ScottyM () -> IO Application
scottyApp defs = do
    s <- MS.execStateT (runS defs) def
    return $ foldl (flip ($)) notFoundApp $ routes s ++ middlewares s
notFoundApp :: Application
notFoundApp _ = return $ ResponseBuilder status404 [("Content-Type","text/html")]
                       $ fromByteString "<h1>404: File Not Found!</h1>"
middleware :: Middleware -> ScottyM ()
middleware m = MS.modify (\ (ScottyState ms rs) -> ScottyState (m:ms) rs)
type Param = (T.Text, T.Text)
data ActionError = Redirect T.Text
                 | ActionError T.Text
                 | Next
    deriving (Eq,Show)
instance Error ActionError where
    strMsg = ActionError . T.pack
data ActionEnv = Env { getReq :: Request, getParams :: [Param], getBody :: BL.ByteString }
newtype ActionM a = AM { runAM :: ErrorT ActionError (ReaderT ActionEnv (MS.StateT Response IO)) a }
    deriving ( Monad, MonadIO, Functor
             , MonadReader ActionEnv, MS.MonadState Response, MonadError ActionError)
runAction :: ActionEnv -> ActionM () -> IO (Maybe Response)
runAction env action = do
    (e,r) <- flip MS.runStateT def
           $ flip runReaderT env
           $ runErrorT
           $ runAM
           $ action `catchError` defaultHandler
    return $ either (const Nothing) (const $ Just r) e
defaultHandler :: ActionError -> ActionM ()
defaultHandler (Redirect url) = do
    status status302
    header "Location" url
defaultHandler (ActionError msg) = do
    status status500
    html $ mconcat ["<h1>500 Internal Server Error</h1>", msg]
defaultHandler Next = next
raise :: T.Text -> ActionM a
raise = throwError . ActionError
next :: ActionM a
next = throwError Next
rescue :: ActionM a -> (T.Text -> ActionM a) -> ActionM a
rescue action handler = catchError action $ \e -> case e of
    ActionError msg -> handler msg      
    other           -> throwError other 
redirect :: T.Text -> ActionM ()
redirect = throwError . Redirect
request :: ActionM Request
request = getReq <$> ask
jsonData :: (A.FromJSON a) => ActionM a
jsonData = do
    body <- getBody <$> ask
    maybe (raise "jsonData: no parse") return $ A.decode body
param :: (Parsable a) => T.Text -> ActionM a
param k = do
    val <- lookup k <$> getParams <$> ask
    case val of
        Nothing -> raise $ mconcat ["Param: ", k, " not found!"]
        Just v  -> either (const next) return $ parseParam v
class Parsable a where
    parseParam :: T.Text -> Either T.Text a
    
    parseParamList :: T.Text -> Either T.Text [a]
    parseParamList t = sequence $ map parseParam (T.split (==',') t)
instance Parsable T.Text where parseParam = Right
instance Parsable B.ByteString where parseParam = Right . lazyTextToStrictByteString
instance Parsable Char where
    parseParam t = case T.unpack t of
                    [c] -> Right c
                    _   -> Left "parseParam Char: no parse"
    parseParamList = Right . T.unpack 
instance Parsable () where
    parseParam t = if T.null t then Right () else Left "parseParam Unit: no parse"
instance (Parsable a) => Parsable [a] where parseParam = parseParamList
instance Parsable Bool where parseParam = readEither
instance Parsable Double where parseParam = readEither
instance Parsable Float where parseParam = readEither
instance Parsable Int where parseParam = readEither
instance Parsable Integer where parseParam = readEither
readEither :: (Read a) => T.Text -> Either T.Text a
readEither t = case [ x | (x,"") <- reads (T.unpack t) ] of
                [x] -> Right x
                []  -> Left "readEither: no parse"
                _   -> Left "readEither: ambiguous parse"
get :: T.Text -> ActionM () -> ScottyM ()
get = addroute GET
post :: T.Text -> ActionM () -> ScottyM ()
post = addroute POST
put :: T.Text -> ActionM () -> ScottyM ()
put = addroute PUT
delete :: T.Text -> ActionM () -> ScottyM ()
delete = addroute DELETE
addroute :: StdMethod -> T.Text -> ActionM () -> ScottyM ()
addroute method path action = MS.modify (\ (ScottyState ms rs) -> ScottyState ms (r:rs))
    where r = route method withSlash action
          withSlash = case T.uncons path of
                        Just ('/',_) -> path
                        _            -> T.cons '/' path
route :: StdMethod -> T.Text -> ActionM () -> Middleware
route method path action app req =
    if Right method == parseMethod (requestMethod req)
    then case matchRoute path (strictByteStringToLazyText $ rawPathInfo req) of
            Just captures -> do
                env <- mkEnv method req captures
                res <- lift $ runAction env action
                maybe tryNext return res
            Nothing -> tryNext
    else tryNext
  where tryNext = app req
mkEnv :: StdMethod -> Request -> [Param] -> ResourceT IO ActionEnv
mkEnv method req captures = do
    body <- BL.fromChunks <$> (lazyConsume $ requestBody req)
    let params = captures ++ formparams ++ queryparams
        formparams = case (method, lookup "Content-Type" [(CI.mk k, CI.mk v) | (k,v) <- requestHeaders req]) of
                        (POST, Just "application/x-www-form-urlencoded") -> parseEncodedParams $ mconcat $ BL.toChunks body
                        _ -> []
        queryparams = parseEncodedParams $ rawQueryString req
    return $ Env req params body
parseEncodedParams :: B.ByteString -> [Param]
parseEncodedParams bs = [ (T.fromStrict k, T.fromStrict $ fromMaybe "" v) | (k,v) <- parseQueryText bs ]
matchRoute :: T.Text -> T.Text -> Maybe [Param]
matchRoute pat req = go (T.split (=='/') pat) (T.split (=='/') req) []
    where go [] [] ps = Just ps 
          go [] r  ps | T.null (mconcat r)  = Just ps 
                      | otherwise           = Nothing 
          go p  [] ps | T.null (mconcat p)  = Just ps 
                      | otherwise           = 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      
status :: Status -> ActionM ()
status = MS.modify . setStatus
header :: T.Text -> T.Text -> ActionM ()
header k v = MS.modify $ setHeader (CI.mk $ lazyTextToStrictByteString k, lazyTextToStrictByteString v)
text :: T.Text -> ActionM ()
text t = do
    header "Content-Type" "text/plain"
    MS.modify $ setContent $ Left $ fromLazyByteString $ encodeUtf8 t
html :: T.Text -> ActionM ()
html t = do
    header "Content-Type" "text/html"
    MS.modify $ setContent $ Left $ fromLazyByteString $ encodeUtf8 t
file :: FilePath -> ActionM ()
file = MS.modify . setContent . Right
json :: (A.ToJSON a) => a -> ActionM ()
json v = do
    header "Content-Type" "application/json"
    MS.modify $ setContent $ Left $ fromLazyByteString $ A.encode v