{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
{-# LANGUAGE TemplateHaskell, QuasiQuotes #-}
{- |
Module      :  Network.Wai.Middleware.Routes.Routes
Copyright   :  (c) Anupam Jain 2013
License     :  MIT (see the file LICENSE)

Maintainer  :  ajnsit@gmail.com
Stability   :  experimental
Portability :  non-portable (uses ghc extensions)

This package provides typesafe URLs for Wai applications.
-}
module Network.Wai.Middleware.Routes.Routes
    ( -- * Quasi Quoters
      parseRoutes            -- | Parse Routes declared inline
    , parseRoutesFile        -- | Parse routes declared in a file
    , parseRoutesNoCheck     -- | Parse routes declared inline, without checking for overlaps
    , parseRoutesFileNoCheck -- | Parse routes declared in a file, without checking for overlaps

    -- * Template Haskell methods
    , mkRoute

    -- * Dispatch
    , routeDispatch

    -- * URL rendering and parsing
    , showRoute
    , readRoute

    -- * Application Handlers
    , Handler

    -- * Generated Datatypes
    , Routable(..)           -- | Used internally. However needs to be exported for TH to work.
    , RenderRoute(..)        -- | A `RenderRoute` instance for your site datatype is automatically generated by `mkRoute`
    , ParseRoute(..)         -- | A `ParseRoute` instance for your site datatype is automatically generated by `mkRoute`
    , RouteAttrs(..)         -- | A `RouteAttrs` instance for your site datatype is automatically generated by `mkRoute`

    , RequestData            -- | An abstract representation of the request data. You can get the wai request object by using `waiReq`
    , waiReq                 -- | Extract the wai `Request` object from `RequestData`
    , nextApp                -- | Extract the next Application in the stack
    , runNext                -- | Run the next application in the stack
    )
    where

-- Conduit
import Data.Conduit (ResourceT)

-- Wai
import Network.Wai (Middleware, Application, pathInfo, requestMethod, requestMethod, Response(ResponseBuilder), Request(..))
import Network.HTTP.Types (decodePath, encodePath, queryTextToQuery, queryToQueryText, status405)

-- Yesod Routes
import Yesod.Routes.Class (Route, RenderRoute(..), ParseRoute(..), RouteAttrs(..))
import Yesod.Routes.Parse (parseRoutes, parseRoutesNoCheck, parseRoutesFile, parseRoutesFileNoCheck, parseType)
import Yesod.Routes.TH (mkRenderRouteInstance, mkParseRouteInstance, mkRouteAttrsInstance, mkDispatchClause, ResourceTree(..), MkDispatchSettings(..), defaultGetHandler)

-- Text and Bytestring
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8, decodeUtf8)
import Blaze.ByteString.Builder (toByteString, fromByteString)

-- TH
import Language.Haskell.TH.Syntax

-- Convenience
import Control.Arrow (second)
import Data.Maybe (fromMaybe)

-- Common ContentTypes
import Network.Wai.Middleware.Routes.ContentTypes

-- An abstract request
data RequestData = RequestData
  { waiReq  :: Request
  , nextApp :: Application
  }

-- | Run the next application in the stack
runNext :: RequestData -> ResourceT IO Response
runNext req = nextApp req $ waiReq req

-- | A `Handler` generates an App from the master datatype
type Handler master = master -> RequestData -> ResourceT IO Response

-- Baked in applications that handle 404 and 405 errors
-- TODO: Inspect the request to figure out acceptable output formats
--   Currently we assume text/plain is acceptable
app404 :: Handler master
app404 _master req = nextApp req $ waiReq req

app405 :: Handler master
app405 _master _req = return $ ResponseBuilder status405 [contentType typePlain] $ fromByteString "405 - Method Not Allowed"

-- | Generates all the things needed for efficient routing,
-- including your application's `Route` datatype, and
--  `RenderRoute`, `ParseRoute`, and `RouteAttr` instances
mkRoute :: String -> [ResourceTree String] -> Q [Dec]
mkRoute typName routes = do
  let typ = parseType typName
  let resourceTrees = map (fmap parseType) routes
  rinst <- mkRenderRouteInstance typ resourceTrees
  pinst <- mkParseRouteInstance typ resourceTrees
  ainst <- mkRouteAttrsInstance typ resourceTrees
  disp  <- mkDispatchClause MkDispatchSettings
        { mdsRunHandler    = [| runHandler             |]
        -- We don't use subsites
        , mdsSubDispatcher = [| undefined              |]
        , mdsGetPathInfo   = [| pathInfo . waiReq      |]
        , mdsMethod        = [| requestMethod . waiReq |]
        -- We don't use subsites
        , mdsSetPathInfo   = [| undefined              |]
        , mds404           = [| app404                 |]
        , mds405           = [| app405                 |]
        , mdsGetHandler    = defaultGetHandler
        } routes
  return $ InstanceD []
          (ConT ''Routable `AppT` typ)
          [FunD (mkName "dispatcher") [disp]]
        : ainst
        : pinst
        : rinst


-- PRIVATE
runHandler
    :: Handler master
    -> master
    -> Maybe (Route master)
    -> RequestData -> ResourceT IO Response -- App
runHandler h master _ = h master

-- | A `Routable` instance can be used in dispatching.
--   An appropriate instance for your site datatype is
--   automatically generated by `mkRoute`
class Routable master where
  dispatcher :: Handler master

-- | Generates the application middleware from a `Routable` master datatype
routeDispatch :: Routable master => master -> Middleware
routeDispatch master def req = dispatcher master RequestData{waiReq=req, nextApp=def}

-- | Renders a `Route` as Text
showRoute :: RenderRoute master => Route master -> Text
showRoute = uncurry encodePathInfo . second (map $ second Just) . renderRoute
  where
    encodePathInfo :: [Text] -> [(Text, Maybe Text)] -> Text
    encodePathInfo segments = decodeUtf8 . toByteString . encodePath segments . queryTextToQuery

-- | Read a route from Text
-- Returns Nothing if Route reading failed. Just route otherwise
readRoute :: ParseRoute master => Text -> Maybe (Route master)
readRoute = parseRoute . second (map (second (fromMaybe "")) . queryToQueryText) . decodePath . encodeUtf8