{-# LANGUAGE BangPatterns #-}
module Webby.Route where

-- This modules contains the data structure and utilities to process
-- route patterns and lookup request paths internally in Webby.

import qualified Data.HashMap.Strict as H

import           Webby.Types
import           WebbyPrelude


data HashTrie a = HashTrie { handlerMay :: Maybe a
                           , litSubtree :: H.HashMap Text (HashTrie a)
                           , capMay     :: Maybe (Text, HashTrie a)
                           }
                deriving (Eq, Show)

emptyHashTrie :: HashTrie a
emptyHashTrie = HashTrie Nothing H.empty Nothing

-- | addItem adds a route to the HashTrie, only if the route does not
-- already exist, and the route does not have overlapping captures.
--
-- An overlapping capture happens when there are two routes with
-- captures at the same position, for example:
--   @/person/:name@
--   @/person/:id@
--
-- Note that @/person/:name@ and @/address/:name@ are not overlapping
-- captures as they have different leading components.
addItem :: ([PathSegment], a) -> HashTrie a -> Maybe (HashTrie a)
addItem ([], !handler) !trie =
    -- If there are no path segments remaining, set handler at the
    -- current node of the HashTrie
    case handlerMay trie of
      Nothing -> Just $ trie { handlerMay = Just handler }
      Just _  -> Nothing
addItem (b:bs, !handler) !trie =
    case b of
      -- For a literal path segment, lookup and insert in the
      -- litSubtree.
      Literal p ->
          let ltrie = litSubtree trie
              child = H.lookupDefault emptyHashTrie p ltrie
          in do nchild <- addItem (bs, handler) child
                let nltrie = H.insert p nchild ltrie
                return $ trie { litSubtree = nltrie }

      -- For a capture path segment, insert into the capture child of
      -- the current node (if it doesn't exist).
      Capture p ->
          case capMay trie of
            Nothing -> do ntrie <- addItem (bs, handler) emptyHashTrie
                          return $ trie { capMay = Just (p, ntrie) }
            Just _ -> Nothing

-- | lookupItem lookup the path segments in the tree and returns a
-- handler if a match is found along with a map of captures found.
lookupItem :: [Text] -> HashTrie a
           -> Maybe (Captures, a)
lookupItem !bs' !trie' = lookupItemWithCaptures bs' trie' H.empty
  where
    lookupItemWithCaptures [] !trie !h = do hdlr <- handlerMay trie
                                            return (h, hdlr)
    lookupItemWithCaptures (b:bs) !trie !h =
        let litChildMay = H.lookup b $ litSubtree trie
            captureLookup = do (capKey, childTrie) <- capMay trie
                               lookupItemWithCaptures bs childTrie $
                                   H.insert capKey b h
        in -- At lookup we always prefer a literal match over a
           -- capture if both are present at node.
          maybe captureLookup
          (\childTrie -> lookupItemWithCaptures bs childTrie h)
          litChildMay

routePattern2PathSegments :: RoutePattern -> [PathSegment]
routePattern2PathSegments (RoutePattern mthd ps) =
    (Literal $ decodeUtf8Lenient mthd) : ps

mkRoutesHashTrie :: Routes a -> Maybe (HashTrie (WebbyM a ()))
mkRoutesHashTrie rs = foldl combine (Just emptyHashTrie) rs
  where

    combine Nothing _ = Nothing
    combine (Just h) (rpat, handler) =
        let pathSegments = routePattern2PathSegments rpat
        in addItem (pathSegments, handler) h