{-# LANGUAGE FlexibleContexts #-}

------------------------------------------------------------------------------
module Snap.Snaplet.Rest.Serve
    ( serveResource
    , serveResourceWith
    ) where

------------------------------------------------------------------------------
import qualified Data.ByteString as BS

------------------------------------------------------------------------------
import Control.Applicative
import Control.Monad
import Data.Maybe
import Snap.Core
import Snap.Snaplet        (Handler)

------------------------------------------------------------------------------
import Snap.Snaplet.Rest.Config
import Snap.Snaplet.Rest.Failure
import Snap.Snaplet.Rest.FromRequest.Internal
import Snap.Snaplet.Rest.Media
import Snap.Snaplet.Rest.Options
import Snap.Snaplet.Rest.Proxy                (Proxy (..))
import Snap.Snaplet.Rest.Resource.Internal


------------------------------------------------------------------------------
-- | Serve the specified resource using the configuration in the monad.
serveResource
    :: (HasResourceConfig b, FromRequest id)
    => Resource res (Handler b b) id diff -> Handler b b ()
serveResource res = getResourceConfig >>= serveResourceWith res


------------------------------------------------------------------------------
-- | Serve the specified resource using the given configuration.
serveResourceWith
    :: (MonadSnap m, FromRequest id)
    => Resource res m id diff -> ResourceConfig m -> m ()
serveResourceWith res' cfg = checkPath $
        serveRoute' GET (handleGet res) (retrieve res)
    <|> serveRoute' POST (handlePost res) (create res)
    <|> serveRoute' DELETE (handleDelete res) (delete res)
    <|> servePut res cfg
    <|> serveRoute' PATCH (handlePatch res) (update res)
    <|> serveRoute' OPTIONS (retrieveOptions parsePath) (Just res)
    <|> serveRoute' HEAD (handleGet res) (retrieve res)
  where
    res = complete res'
    serveRoute' = serveRoute res cfg
    checkPath = if pathEnabled' res Proxy
        then id else (ifNotTop (pathFailure cfg) <|>)


------------------------------------------------------------------------------
-- | Serves a route for the given method if the Maybe value is Just, otherwise
-- serves a method failure error.
serveRoute
    :: MonadSnap m
    => Resource res m id diff -> ResourceConfig m -> Method
    -> (ResourceConfig m -> a -> m b) -> Maybe a -> m b
serveRoute res cfg mt rt mf = method mt $
    maybe (methodFailure res cfg) (rt cfg) mf


------------------------------------------------------------------------------
-- | Produces a PUT response depending on the PutAction in the resource.
servePut
    :: (MonadSnap m, FromRequest id)
    => Resource res m id diff -> ResourceConfig m -> m ()
servePut res cfg =
    (ifTop (serveRoute' (replaceResources res) (both create delete)) <|>) $
    ifNotTop $ case putAction res of
        Just Create -> serveRoute' (createResource res) (create res)
        Just Update -> serveRoute' updatePut (both update toDiff)
        Nothing     -> serveRoute' (tryUpdateResource res)
            ((,,) <$> create res <*> update res <*> toDiff res)
  where
    serveRoute' = serveRoute res cfg PUT
    both f g = (,) <$> f res <*> g res
    updatePut _ (update', toDiff') = updateResource
        (fmap toDiff' . receive (parsers res)) cfg update'


------------------------------------------------------------------------------
-- | Routes to 'retrieveResources' and 'retrieveResource'.
handleGet
    :: (MonadSnap m, FromRequest id)
    => Resource res m id diff -> ResourceConfig m -> (id -> m [res])
    -> m ()
handleGet res cfg retrieve' =
    ifTop (retrieveResources res cfg retrieve') <|> retrieveResource res cfg retrieve'


------------------------------------------------------------------------------
-- | Retrieves and serves resources using the URL query string.
retrieveResources
    :: MonadSnap m
    => Resource res m id diff -> ResourceConfig m -> (id -> m [res])
    -> m ()
retrieveResources res cfg retrieve' = parseParams res cfg >>= maybe
    (queryFailure cfg) (retrieve' >=> serve (listRenderers res) cfg . limit)
  where limit = maybe id take $ readLimit cfg


------------------------------------------------------------------------------
-- | Retrieve and serve a resource using the remaining path information.
retrieveResource
    :: (MonadSnap m, FromRequest id)
    => Resource res m id diff -> ResourceConfig m -> (id -> m [res])
    -> m ()
retrieveResource res cfg retrieve' = parsePath >>= maybe (pathFailure cfg)
    (retrieve' >=> maybe (lookupFailure cfg)
        (serve (renderers res) cfg) . listToMaybe)


------------------------------------------------------------------------------
-- | Routes to 'createResource' if there is no remaining path information,
-- otherwise indicates that POST is not allowed directly on a resource.
handlePost
    :: MonadSnap m
    => Resource res m id diff -> ResourceConfig m -> (res -> m ()) -> m ()
handlePost res cfg create' =
    ifTop (createResource res cfg create') <|> methodFailure res cfg


------------------------------------------------------------------------------
-- | Create a new resource from the request body.
createResource
    :: MonadSnap m
    => Resource res m id diff -> ResourceConfig m -> (res -> m ()) -> m ()
createResource res cfg create' = receive (parsers res) cfg >>= create'


------------------------------------------------------------------------------
-- | Replaces all matched resources with the ones from the request body.
-- Retrieves and parses the request body before proceeding to delete.  Note
-- that this operation is non-atomic.
replaceResources
    :: MonadSnap m
    => Resource res m id diff -> ResourceConfig m
    -> (res -> m (), id -> m Bool) -> m ()
replaceResources res cfg (create', delete') = do
    m <- receive (listParsers res) cfg
    deleteResources res cfg delete'
    mapM_ create' m


------------------------------------------------------------------------------
-- | Updates resources from the request body.
updateResources
    :: MonadSnap m
    => Resource res m id diff -> ResourceConfig m
    -> (id -> diff -> m Bool) -> m ()
updateResources res cfg update' = do
    search <- parseParams res cfg >>= maybe (queryFailure cfg) return
    receive (diffParsers res) cfg >>= void . update' search


------------------------------------------------------------------------------
-- | Update a resource from the request body.  Sends a lookup failure if the
-- update failed.
updateResource
    :: (MonadSnap m, FromRequest id)
    => (ResourceConfig m -> m diff) -> ResourceConfig m
    -> (id -> diff -> m Bool) -> m ()
updateResource receive' cfg update' = receive' cfg >>=
    updateResource' cfg update' >>= flip unless (lookupFailure cfg)


------------------------------------------------------------------------------
-- | Update a resource with the given value.  Returns 'True' if the update was
-- successful.
updateResource'
    :: (MonadSnap m, FromRequest id)
    => ResourceConfig m -> (id -> diff -> m Bool) -> diff -> m Bool
updateResource' cfg update' diff = parsePath >>=
    maybe (pathFailure cfg) (`update'` diff)


------------------------------------------------------------------------------
-- | Attempts to update with the request body, and creates it instead if that
-- fails.
tryUpdateResource
    :: (MonadSnap m, FromRequest id)
    => Resource res m id diff -> ResourceConfig m
    -> (res -> m (), id -> diff -> m Bool, res -> diff) -> m ()
tryUpdateResource res cfg (create', update', toDiff') = do
    par <- receive (parsers res) cfg
    updateResource' cfg update' (toDiff' par) >>= flip unless (create' par)


------------------------------------------------------------------------------
-- | Routes to 'updateResources' and 'updateResource', ensuring that PATCH is
-- not disabled in the latter case.
handlePatch
    :: (MonadSnap m, FromRequest id)
    => Resource res m id diff -> ResourceConfig m
    -> (id -> diff -> m Bool) -> m ()
handlePatch res cfg update' =
    ifTop (updateResources res cfg update') <|> do
        unless (patchEnabled res) $ methodFailure res cfg
        updateResource (receive $ diffParsers res) cfg update'


------------------------------------------------------------------------------
-- | Routes to 'deleteResources' and 'deleteResource'.
handleDelete
    :: (MonadSnap m, FromRequest id)
    => Resource res m id diff -> ResourceConfig m -> (id -> m Bool) -> m ()
handleDelete res cfg delete' =
    ifTop (deleteResources res cfg delete') <|> deleteResource cfg delete'


------------------------------------------------------------------------------
-- | Deletes the resources matching the URL query string.
deleteResources
    :: MonadSnap m
    => Resource res m id diff -> ResourceConfig m -> (id -> m Bool)
    -> m ()
deleteResources res cfg delete' = parseParams res cfg >>=
    maybe (queryFailure cfg) (void . delete')


------------------------------------------------------------------------------
-- | Deletes the resource at the current path.
deleteResource
    :: (MonadSnap m, FromRequest id)
    => ResourceConfig m -> (id -> m Bool) -> m ()
deleteResource cfg delete' = parsePath >>= maybe (pathFailure cfg)
    (delete' >=> flip unless (lookupFailure cfg))


------------------------------------------------------------------------------
-- | Serves either collection or resource options, depending on the path.
retrieveOptions
    :: MonadSnap m
    => m (Maybe id) -> ResourceConfig m
    -> Resource res m id diff -> m ()
retrieveOptions parsePath' cfg res = do
    isTop >>=
        flip unless (isNothing <$> parsePath' >>= flip when (pathFailure cfg))
    setAllow $ optionsFor res
    modifyResponse $ setContentLength 0


------------------------------------------------------------------------------
-- | Retrieve the remaining path info and parse it into the identifier type.
parsePath :: (MonadSnap m, FromRequest id) => m (Maybe id)
parsePath = fromPath . rqPathInfo <$> getRequest


------------------------------------------------------------------------------
-- | Retrieve the URL query string and parse it into the identifier type.
parseParams
    :: MonadSnap m
    => Resource res m id diff -> ResourceConfig m -> m (Maybe id)
parseParams res cfg = maybe (methodFailure res cfg) tryParse (fromParams res)
  where tryParse fromParams' = fromParams' . rqParams <$> getRequest


------------------------------------------------------------------------------
-- | Grabs the appropriate instance of 'pathEnabled' for the given resource.
pathEnabled'
    :: FromRequest id => Resource res m id diff -> Proxy id -> Bool
pathEnabled' _ = pathEnabled


------------------------------------------------------------------------------
-- | Determines if 'rqPathInfo' is null.
isTop :: MonadSnap m => m Bool
isTop = BS.null . rqPathInfo <$> getRequest


------------------------------------------------------------------------------
-- | Opposite of 'ifTop', runs the given action to 'rqPathInfo' is not null.
ifNotTop :: MonadSnap m => m a -> m a
ifNotTop m = do
    top <- isTop
    if top then pass else m