{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE BangPatterns #-}

-- | This module contains helpers for use "Yesod.Routes.Dispatch" with 
--   "Network.Wai".
--
--   This 'Middleware' uses first 'D.Piece' in path to route @HTTP@ method.
--   'D.Static' means concrete method. 'D.Dynamic' means any method.

module Network.Wai.Middleware.Route (
    -- * Routing rules
    Rule(..),
    mkRoutes,
    mkRoutes',
    mkRoute,
    
    -- * Middleware
    dispatch,
    dispatch_
) where

import Control.Applicative ((<$>), (<*>))

import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8)

import Network.Wai (Application, requestMethod, pathInfo)
import qualified Yesod.Routes.Dispatch as D

-- | Rule for route. Rules without single quotes (@'@) means fixed length
--   paths. And vice versa, rules with single quotes (@'@) means paths with
--   variable lengh 
--
--   Paths converts to 'D.Piece's by following rules:
--   
--   * Paths splits by slashes (@/@).
--   
--   * Text between slashes becomes 'D.Static' 'D.Piece'. The same thing 
--     happens with the text at the ends of paths.
--
--   * Hashes (@#@) inside slashes becomes 'D.Dynamic' 'D.Piece's.
--
--   * To make route with variable length just add asterisk (@\*@) after last
--     slash.
--
-- > "foo"
-- > [Static "foo"] Fixed
-- > 
-- > "foo/bar"
-- > [Static "foo", Static "bar"] Fixed
-- > 
-- > "foo/#/bar"
-- > [Static "foo", Dynamic, Static "bar"] Fixed
-- > 
-- > "foo/#/bar/baz/*"
-- > [Dynamic, Static "foo", Dynamic, Static "bar", Static "baz"] Variable

data Rule = 
    Get T.Text Application          -- ^ @GET@ method
  | Post T.Text Application         -- ^ @POST@ method
  | Head T.Text Application         -- ^ @HEAD@ method
  | Put T.Text Application          -- ^ @PUT@ method
  | Delete T.Text Application       -- ^ @DELETE@ method
  | Trace T.Text Application        -- ^ @TRACE@ method
  | Connect T.Text Application      -- ^ @CONNECT@ method
  | Options T.Text Application      -- ^ @OPTIONS@ method
  | Any T.Text Application          -- ^ Any @HTTP@ method
  | Gen T.Text T.Text Application
        -- ^ Generic rule with @HTTP@ method and path

-- | Make 'D.Route's from 'Rules'. 
--   
--   Equivalent @map mkRoute@ 
mkRoutes :: 
       [Rule]
            -- ^ Routing rules 
    -> [D.Route Application]
mkRoutes = map mkRoute
{-# INLINE mkRoutes #-} 

-- | Make 'D.Dispatch's from 'Rules'. 
--   
--   Equivalent @toDispatch . mkRoutes@ 
mkRoutes' :: [Rule] -> D.Dispatch Application
mkRoutes' = D.toDispatch . mkRoutes

-- | Make 'D.Route' from 'Rule'. 'D.rhPieces' of 'D.Route' will be
--   prepended with 'D.Piece' with corresponding @HTTP@ method. 
--   'D.Static' means concrete method. 'D.Dynamic' means any method.
--
-- > mkRoute $ Get "foo/bar" app
-- > Route [Static "foo", Static "bar"] False (const $ Just app) 
mkRoute :: Rule -> D.Route Application
mkRoute (Get p a) = mkGenRoute (D.Static "GET") p a
mkRoute (Post p a) = mkGenRoute (D.Static "POST") p a
mkRoute (Head p a) = mkGenRoute (D.Static "HEAD") p a
mkRoute (Put p a) = mkGenRoute (D.Static "PUT") p a
mkRoute (Delete p a) = mkGenRoute (D.Static "DELETE") p a
mkRoute (Trace p a) = mkGenRoute (D.Static "TRACE") p a
mkRoute (Connect p a) = mkGenRoute (D.Static "CONNECT") p a
mkRoute (Options p a) = mkGenRoute (D.Static "OPTIONS") p a
mkRoute (Any p a) = mkGenRoute D.Dynamic p a
mkRoute (Gen m p a) = mkGenRoute (D.Static m) p a
{-# INLINE mkRoute #-} 

-- | Make generic route
mkGenRoute :: 
       D.Piece      -- ^ Method piece. 'D.Dynamic' means any method. 
    -> T.Text         -- ^ Path pieces
    -> Application  -- ^ Routed application
    -> D.Route Application
mkGenRoute m path =
    let (!pieces, !hasMulti) = mkPieces path in
    D.Route (m:pieces) hasMulti . const . Just 
{-# INLINE mkGenRoute #-}

-- | Parse 'T.Text' and make tuple with 'D.Piece's and 'D.hasMulti'. 
--   
-- > ""         -- ([], False)
-- > "*"        -- ([], True)
-- > "foo/#"    -- ([Static "foo", Dynamic], False)  
-- > "foo/#/*"  -- ([Static "foo", Dynamic], True)  
mkPieces :: 
       T.Text               -- ^ Path to parse 
    -> ([D.Piece], Bool)    -- ^ list of 'D.Piece's and 'D.hasMulti'
mkPieces "" = ([], False) 
mkPieces !t = 
    if T.last t == '*' 
        then (prep . T.init $ t, True)
        else (prep t, False)
  where
    prep = map chunk . filter (/="") . T.split (=='/')
    {-# INLINE prep #-} 
    -- | Convert chunk
    chunk "#" = D.Dynamic
    chunk c = D.Static c
    {-# INLINE chunk #-} 
{-# INLINE mkPieces #-} 

-----------------------------------------------------------------------------
-- Middleware.
-----------------------------------------------------------------------------

-- | Dispatch 'Middleware'. 
-- 
-- > rs :: Dispatch Application
-- > rs = toDispatch . mkRoutes [
-- >      Get  "foo"  fooGetApp
-- >    , Post "foo"  fooPostApp
-- >    , Get "foo//bar" fooDynBarApp
-- > 
-- >    , Any  "any"  anyMethodApp
-- >    ]
-- > 
-- > app :: Application
-- > app = dispatch True rs (error "Not dispatched")
dispatch :: 
       Bool 
            -- ^ Squash empty 'pathInfo' chunks. It often appear in the 
            --   presence of double slashes or \"Ending slash\" in URL.
    -> D.Dispatch Application   
            -- ^ Dispatch function. 
            --   Use 'D.toDispatch' and route helpers.
    -> Application 
            -- ^ Default (@404@) application. 
    -> Application
dispatch squash mappings defApp req = 
    case mappings . needle $ req of
        Nothing -> defApp req
        (Just app) -> app req 
  where
    needle = (:) <$> decodeUtf8 . requestMethod <*> path squash
    path False = pathInfo
    path True = filter (/="") . pathInfo

-- | Dispatch 'Middleware' with auto-squash empty path pieces. Equiwalent to
-- > dispatch True
dispatch_ ::
       D.Dispatch Application   
            -- ^ Dispatch function. 
            --   Use 'D.toDispatch' and route helpers.
    -> Application 
            -- ^ Default (@404@) application. 
    -> Application
dispatch_ = dispatch True