{-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}

module Airship.Internal.Route where

import Airship.Resource

import Data.Monoid
import Data.Foldable (foldr')
import Data.Text (Text)
import Data.HashMap.Strict (HashMap, insert)

#if __GLASGOW_HASKELL__ < 710
import           Control.Applicative
#endif
import Control.Monad.Identity
import Control.Monad.Writer (Writer, WriterT (..), execWriter)
import Control.Monad.Writer.Class (MonadWriter)

import Data.String (IsString, fromString)

-- | 'Route's represent chunks of text used to match over URLs.
-- You match hardcoded paths with string literals (and the @-XOverloadedStrings@ extension),
-- named variables with the 'var' combinator, and wildcards with 'star'.
newtype Route = Route { getRoute :: [BoundOrUnbound] } deriving (Show, Monoid)

data BoundOrUnbound = Bound Text
                    | Var Text
                    | RestUnbound deriving (Show)

instance IsString Route where
    fromString s = Route [Bound (fromString s)]

runRouter :: RoutingSpec m a -> [(Route, Resource m)]
runRouter routes = execWriter (getRouter routes)

-- | @a '</>' b@ separates the path components @a@ and @b@ with a slash.
-- This is actually just a synonym for 'mappend'.
(</>) :: Route -> Route -> Route
(</>) = (<>)

-- | Represents the root resource (@/@). This should usually be the first path declared in a 'RoutingSpec'.
root :: Route
root = Route []

-- | Captures a named in a route and adds it to the 'routingParams' hashmap under the provided 'Text' value. For example,
--
-- @
--    "blog" '</>' 'var' "date" '</>' 'var' "post"
-- @
--
-- will capture all URLs of the form @\/blog\/$date\/$post@, and add @date@ and @post@ to the 'routingParams'
-- contained within the resource this route maps to.
var :: Text -> Route
var t = Route [Var t]

-- | Captures a wildcard route. For example,
--
-- @
--    "emcees" '</>' star
-- @
--
-- will match @\/emcees@, @\/emcees/biggie@, @\/emcees\/earl\/vince@, and so on and so forth.
star :: Route
star = Route [RestUnbound]

-- | Represents a fully-specified set of routes that map paths (represented as 'Route's) to 'Resource's. 'RoutingSpec's are declared with do-notation, to wit:
--
-- @
--    myRoutes :: RoutingSpec IO ()
--    myRoutes = do
--      root                                 #> myRootResource
--      "blog" '</>' var "date" '</>' var "post" #> blogPostResource
--      "about"                              #> aboutResource
--      "anything" '</>' star                  #> wildcardResource
-- @
--
newtype RoutingSpec m a = RoutingSpec { getRouter :: Writer [(Route, Resource m)] a }
    deriving (Functor, Applicative, Monad, MonadWriter [(Route, Resource m)])

route :: [(Route, a)] -> [Text] -> a -> (a, (HashMap Text Text, [Text]))
route routes pInfo resource404 = foldr' (matchRoute pInfo) (resource404, (mempty, mempty)) routes

matchRoute :: [Text] -> (Route, a) -> (a, (HashMap Text Text, [Text])) -> (a, (HashMap Text Text, [Text]))
matchRoute paths (rSpec, resource) (previousMatch, previousMap) =
    case matchesRoute paths rSpec of
      Nothing -> (previousMatch, previousMap)
      Just m  -> (resource, m)

matchesRoute :: [Text] -> Route -> Maybe (HashMap Text Text, [Text])
matchesRoute paths spec = matchesRoute' paths (getRoute spec) (mempty, mempty) False where
    -- recursion is over, and we never bailed out to return false, so we match
    matchesRoute' []        []              acc     _   = Just acc
    -- there is an extra part of the path left, and we don't have more matching
    matchesRoute' (_ph:_ptl) []             _       _   = Nothing
    -- we match whatever is left, so it doesn't matter what's left in the path
    matchesRoute' r        (RestUnbound:_) (h, d)  _   = Just (h, d ++ r)
    -- we match a specific string, and it matches this part of the path,
    -- so recur
    matchesRoute' (ph:ptl)  (Bound sh:stt)  (h, dispatch) True
        | ph == sh
                                                    = matchesRoute' ptl stt (h, dispatch ++ [ph]) True
    matchesRoute' (ph:ptl)  (Bound sh:stt)  (h, dispatch) False
        | ph == sh
                                                    = matchesRoute' ptl stt (h, dispatch) False
    matchesRoute' (ph:ptl)  (Var t:stt)     acc   _ = matchesRoute' ptl stt (insert t ph (fst acc), snd acc) True
    matchesRoute' _         _               _acc  _ = Nothing