{-# LANGUAGE CPP               #-}
{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes        #-}
{-# LANGUAGE RecordWildCards   #-}

module Airship.Internal.Helpers
    ( parseFormData
    , contentTypeMatches
    , redirectTemporarily
    , redirectPermanently
    , resourceToWai
    , resourceToWaiT
    , resourceToWaiT'
    , appendRequestPath
    , lookupParam
    , lookupParam'
    ) where

#if __GLASGOW_HASKELL__ < 710
import           Control.Applicative
#endif
import           Control.Monad             (join)
import           Data.ByteString           (ByteString, intercalate)
import qualified Data.ByteString.Lazy      as LB
import           Data.Maybe
#if __GLASGOW_HASKELL__ < 710
import           Data.Monoid
#endif
import           Data.Foldable             (forM_)
import qualified Data.HashMap.Strict       as HM
import qualified Data.Map.Strict           as M
import           Data.Text                 (Text)
import           Data.Text.Encoding        (decodeUtf8)
import           Data.Time                 (getCurrentTime)
import           Lens.Micro                ((^.))
import           Network.HTTP.Media
import qualified Network.HTTP.Types        as HTTP
import qualified Network.Wai               as Wai

import           Network.Wai.Parse
import           System.Random

import           Airship.Config
import           Airship.Headers
import           Airship.Internal.Decision
import           Airship.Internal.Route
import           Airship.Resource
import           Airship.Types

-- | Parse form data uploaded with a @Content-Type@ of either
-- @www-form-urlencoded@ or @multipart/form-data@ to return a
-- list of parameter names and values and a list of uploaded
-- files and their information.
parseFormData :: Request -> IO ([Param], [File LB.ByteString])
parseFormData r = parseRequestBody lbsBackEnd r

-- | Returns @True@ if the request's @Content-Type@ header is one of the
-- provided media types. If the @Content-Type@ header is not present,
-- this function will return True.
contentTypeMatches :: Monad m => [MediaType] -> Webmachine m Bool
contentTypeMatches validTypes = do
    headers <- requestHeaders <$> request
    let cType = lookup HTTP.hContentType headers
    return $ case cType of
        Nothing -> True
        Just t  -> isJust $ matchContent validTypes t

-- | Issue an HTTP 302 (Found) response, with `location' as the destination.
redirectTemporarily :: Monad m => ByteString -> Webmachine m a
redirectTemporarily location =
    addResponseHeader ("Location", location) >> halt HTTP.status302

-- | Issue an HTTP 301 (Moved Permantently) response,
-- with `location' as the destination.
redirectPermanently :: Monad m => ByteString -> Webmachine m a
redirectPermanently location =
    addResponseHeader ("Location", location) >> halt HTTP.status301

toWaiResponse :: Response -> AirshipConfig -> ByteString -> ByteString -> Wai.Response
toWaiResponse Response{..} cfg trace quip =
    case _responseBody of
        (ResponseBuilder b) ->
            Wai.responseBuilder _responseStatus headers b
        (ResponseFile path part) ->
            Wai.responseFile _responseStatus headers path part
        (ResponseStream streamer) ->
            Wai.responseStream _responseStatus headers streamer
        Empty ->
            Wai.responseBuilder _responseStatus headers mempty
    where
        headers = traced ++ quipHeader ++ _responseHeaders
        traced  = if cfg^.includeTraceHeader == IncludeHeader
                      then [("Airship-Trace", trace)]
                      else []

        quipHeader  = if cfg^.includeQuipHeader == IncludeHeader
                      then [("Airship-Quip", quip)]
                      else []

-- | Given a 'RoutingSpec', a 404 resource, and a user state @s@, construct a WAI 'Application'.
resourceToWai :: AirshipConfig
              -> RoutingSpec IO ()
              -> ErrorResponses IO
              -> Wai.Application
resourceToWai cfg routes errors =
    resourceToWaiT cfg (const id) routes errors

-- | Given a 'AirshipConfig, a function to modify the 'Response' based on the
-- 'AirshipRequest' and the 'Response' (like WAI middleware), a 'RoutingSpec,
-- and 'ErrorResponses' construct a WAI 'Application'.
resourceToWaiT :: Monad m
               => AirshipConfig
               -> (AirshipRequest -> m Wai.Response -> IO Wai.Response)
               -> RoutingSpec m ()
               -> ErrorResponses m
               -> Wai.Application
resourceToWaiT cfg run routes errors req respond =
    resourceToWaiT' cfg run (runRouter routes) errors req respond

-- | Like 'resourceToWaiT', but expects the 'RoutingSpec' to have been
-- evaluated with 'runRouter'. This is more efficient than 'resourceToWaiT', as
-- the routes will not be evaluated on every request.
-- 
-- Given @routes :: RoutingSpec IO ()@, 'resourceToWaiT'' can be invoked like so:
--
-- > resourceToWaiT' cfg (const id) (runRouter routes) errors
resourceToWaiT' :: Monad m
               => AirshipConfig
               -> (AirshipRequest -> m Wai.Response -> IO Wai.Response)
               -> Trie (RouteLeaf m)
               -> ErrorResponses m
               -> Wai.Application
resourceToWaiT' cfg run routeMapping errors req respond = do
    let pInfo = Wai.rawPathInfo req
    quip <- getQuip
    nowTime <- getCurrentTime
    let (er, (reqParams, dispatched), routePath', r) =
         case route routeMapping pInfo of
             Nothing ->
                 (errors, (mempty, []), decodeUtf8 pInfo, return $ Response HTTP.status404 [(HTTP.hContentType, "text/plain")] Empty)
             Just (RoutedResource rPath resource, pm) ->
                 (M.union (errorResponses resource) errors, pm, routeText rPath, flow resource)
        airshipReq = AirshipRequest req routePath'
        requestReader = RequestReader nowTime airshipReq
        startingState = ResponseState [] Empty reqParams dispatched []
    respond =<< run airshipReq (do
        (response, trace) <-
            eitherResponse requestReader startingState (r >>= errorResponse er)
        return $ toWaiResponse response cfg (traceHeader trace) quip)

-- | If the Response body is Empty the response body is set based on the error responses
--  provided by the application and resource. If the response body is not Empty or
--  there are no error response configured for the status code in the Response then no
--  action is taken. The contents of the 'Webmachine'' response body will be streamed
--  back to the client.
errorResponse :: Monad m =>
                 ErrorResponses m
              -> Response
              -> Webmachine m Response
errorResponse errResps r@Response{..}
    | (HTTP.statusIsClientError _responseStatus ||
       HTTP.statusIsServerError _responseStatus) &&
       isResponseBodyEmpty _responseBody = do
           req <- request
           let reqHeaders = requestHeaders req
               acceptStr = lookup HTTP.hAccept reqHeaders
               errBodies = map dupContentType <$> M.lookup _responseStatus errResps
               mResp = join $ mapAcceptMedia <$> errBodies <*> acceptStr
           forM_ mResp $ \(ct, body) -> do
               putResponseBody =<< body
               addResponseHeader ("Content-Type", renderHeader ct)
           Response
               <$> return _responseStatus
               <*> getResponseHeaders
               <*> getResponseBody
    | otherwise = return r
    where
        isResponseBodyEmpty Empty = True
        isResponseBodyEmpty _ = False
        dupContentType (a, b) = (a, (a, b))


getQuip :: IO ByteString
getQuip = do
  idx <- randomRIO (0, length quips - 1)
  return $ quips !! idx
  where quips = [ "never breaks eye contact"
                , "blame me if inappropriate"
                , "firm pat on the back"
                , "sharkfed"
                , "$300,000 worth of cows"
                , "RB_GC_GUARD"
                , "evacuation not done in time"
                , "javascript doesn't have integers"
                , "WARNING: ulimit -n is 1024"
                , "shut it down"
                ]

traceHeader :: [ByteString] -> ByteString
traceHeader = intercalate ","

-- | Lookup routing parameter and return 500 Internal Server Error if not found.
-- Not finding the paramter usually means the route doesn't match what
-- the resource is expecting.
lookupParam :: Monad m => Text -> Webmachine m Text
lookupParam p = lookupParam' p >>= maybe (halt HTTP.status500) pure

-- | Lookup routing parameter.
lookupParam' :: Monad m => Text -> Webmachine m (Maybe Text)
lookupParam' p = HM.lookup p <$> params