-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.

{-# LANGUAGE
    BangPatterns
  , DeriveGeneric
  , DeriveAnyClass
  , DerivingStrategies
  , TupleSections
#-}

-- | A variant of a (radix) trie with the following characteristics:
--
--   * Keys are simple 'Pattern's composed of 'Matcher's and hence a single key
--     can match multiple input 'Str'ings.
--   * Looking up a 'match' for a 'Str'ing can 'Capture' parts of the string.
--   * Both patterns  and input strings are understood as being composed of
--     (indivisible) chunks of strings of a generic type @s@ (typically
--     instantiated to either 'Text' or 'ByteString'). More precisely, every
--     chunk of an input 'Str'ing is tested against a 'Matcher' of a 'Pattern'
--     in sequence. As a result, pattern tries usually end up less compact than
--     more general tries, since sharing of prefixes as well as all operations
--     are limited to the granularity of these chunks.
--
-- These characteristics hint at the primary intended use-case, whereby
-- keys have a \"natural\" decomposition into chunks and the same
-- chunks are heavily shared by different keys, e.g. as in directory trees.
-- A pattern trie allows to associate values with simple patterns, whereby a
-- single value can essentially be looked up by all strings matching a pattern,
-- thereby capturing parts of it.
--
-- __Strictness:__
-- A 'Trie' is strict in the spine as well as the values (WHNF).
--
-- __Ordering:__ The order of keys and thus elements is unspecified.
-- No particular order may be assumed by folds and traversals, whose
-- combining operations should hence be commutative.
--
-- __Example:__
--
-- >>> :set -XOverloadedStrings
--
-- >>> import Data.ByteString (ByteString)
--
-- >>> let p1 = mempty |> EqStr "home" |> EqStr "alice" |> EqStr "tmp"
-- >>> let p2 = mempty |> EqStr "home" |> AnyStr        |> EqStr "music"
-- >>> let p3 = mempty |> EqStr "data" |> EqStr "bob"   |> EqStr "books"
-- >>> let p4 = mempty |> EqStr "data" |> AnyStr        |> EqStr "books"
-- >>> let p5 = mempty |> EqStr "data" |> AnyStr        |> EqStr "books" |> EqStr "sicp"
--
-- >>> let trie = fromAssocList $ [p1,p2,p3,p4,p5] `zip` [1..] :: Trie ByteString Int
--
-- >>> match ["home","alice","tmp"] trie
-- Just (1,fromList [])
--
-- >>> match ["home","bob","tmp"] trie
-- Nothing
--
-- >>> match ["home","alice","music"] trie
-- Just (2,fromList [Capture {captured = "alice"}])
--
-- >>> match ["home","bob","music"] trie
-- Just (2,fromList [Capture {captured = "bob"}])
--
-- >>> match ["data","bob","books"] trie
-- Just (3,fromList [])
--
-- >>> match ["data","alice","books"] trie
-- Just (4,fromList [Capture {captured = "alice"}])
--
-- >>> match ["data","alice","books","sicp"] trie
-- Just (5,fromList [Capture {captured = "alice"}])
--
-- >>> match ["data","bob","books","sicp"] trie
-- Just (5,fromList [Capture {captured = "bob"}])
--
-- >>> matchPrefix ["data","alice","books","wonderland"] trie
-- Just (4,fromList [Capture {captured = "alice"}],["wonderland"])
--
-- >>> matchPrefix ["data","bob","books","wonderland"] trie
-- Just (4,fromList [Capture {captured = "bob"}],["wonderland"])
--
-- >>> let (t,c,s) = matchPrefixTrie ["data","bob","books","wonderland"] trie
-- >>> (value t, c, s)
-- (Just 4,fromList [Capture {captured = "bob"}],["wonderland"])
--
module Data.Trie.Pattern
    ( Trie, value

    -- * Patterns
    -- $definitions
    , Pattern, Str, Matcher (..), Capture (..)

    -- ** Testing Patterns
    , overlapping
    , MatchOrd (..), MatchPrefixOrd (..)
    , matchOrd, matchPrefixOrd
    , apply
    , applyCapture
    , unapplyCapture
    , applyMatch
    , applyMatches
    , applyMatchPrefix
    , applyMatchesPrefix

    -- * List conversion
    , fromAssocList
    , toAssocList

    -- * Modifications
    , insert
    , adjust
    , delete

    -- * 'Pattern' lookup
    -- $lookupBigO
    , lookup
    , lookupPrefix
    , lookupPrefixTrie

    -- * 'Str'ing matching
    -- $matchingBigO
    , match
    , matchPrefix
    , matchPrefixTrie

    -- * Special folds and traversals
    , traverseWithKey
    , foldMapWithKey
    , foldrWithKey

    -- * Re-exports
    , (|>)
    ) where

import GHC.Generics (Generic)

import Control.Applicative
import Control.DeepSeq (NFData)
import Control.Monad ((<$!>))
import Data.ByteString (ByteString)
import Data.Coerce (coerce)
import Data.Foldable
import Data.Hashable
import Data.List (foldl')
import Data.HashMap.Strict (HashMap)
import Data.Maybe (fromMaybe, isJust)
import Data.Semigroup
import Data.Sequence (Seq (..), (|>))
import Data.Text (Text)
import Prelude hiding (lookup)

import qualified Data.HashMap.Strict as HashMap
import qualified Data.Sequence       as Seq
import qualified Data.Traversable    as Traversable

-- {-# ANN module "HLint: ignore Eta reduce" #-}

-- | An unordered map from 'Pattern's of strings of type @s@ to values
-- of type @a@.
data Trie s a = Trie
    { strtries :: !(HashMap s (Trie s a))
    , vartrie  :: !(Maybe (Trie s a))
    , value    :: !(Maybe a)
        -- ^ The value at the root of the trie, i.e.
        --
        -- @
        -- value t == 'lookup' mempty t
        -- @
    } deriving stock (Eq, Show, Read, Generic)
      deriving anyclass NFData

instance Traversable (Trie s) where
    traverse f = traverseWithKey (const f)

instance Functor (Trie s) where
    fmap = Traversable.fmapDefault

instance Foldable (Trie s) where
  foldMap = Traversable.foldMapDefault

  null (Trie a Nothing Nothing) = HashMap.null a
  null _                        = False
  {-# INLINE null #-}

-- | /Note (left preference)/: If two tries have a value attached to
-- the same 'Pattern' (i.e. to the same key), then @t1 <> t2@ preserves
-- the value of @t1@.
instance (Eq s, Hashable s) => Semigroup (Trie s a) where
    a <> b =  Trie (HashMap.unionWith (<>) (strtries a) (strtries b))
                   (vartrie a <> vartrie b)
                   (value a <|> value b)

    stimes = stimesIdempotent

-- | /Note/: @mappend = (<>)@.
instance (Eq s, Hashable s) => Monoid (Trie s a) where
    mempty  = Trie mempty Nothing Nothing
    mappend = (<>)

-----------------------------------------------------------------------------
-- Patterns

-- $definitions
-- __Definition (/Prefix Match/):__ A 'Str'ing is a /prefix match/ for a
-- 'Pattern', iff all 'Matcher's in the pattern succeed when applied on
-- the chunks of the input string in sequence. A /proper prefix match/
-- is a prefix match that is not a (full) match.
--
--  A prefix match is witnessed by 'applyMatchesPrefix'.
--
-- __Definition (/(Full) Match/):__ A 'Str'ing is a /(full) match/ for a
-- 'Pattern', iff it is a prefix match and there are no unmatched
-- remaining chunks of the input (i.e. the string and the pattern have
-- the same length).
--
--  A (full) match is witnessed by 'applyMatches'.
--
-- __Definition (/Overlapping Patterns/):__
-- Two patterns are /overlapping/, iff they are not equal and
-- there exists an input 'Str'ing that is a (full) match for both patterns.
--
-- Overlapping patterns are witnessed by 'overlapping'.

-- | A pattern is a sequence of 'Matcher's and serves as a key in a pattern
-- trie.
--
-- If two patterns are overlapping for an input string, the preference for
-- a 'match' is given by the partial order @EqStr > AnyStr@ on the competing
-- matchers, i.e. towards the more specific pattern. This partial order is
-- witnessed and subsumed by the total order 'MatchOrd'.
--
-- The preference for a prefix match is reversed, i.e. for an input string where
-- only a proper prefix is a match for overlapping patterns, the preference
-- is given by the partial order @AnyStr > EqStr@, i.e. towards the more general
-- pattern. This partial order is witnessed and subsumed by the total order
-- 'PrefixMatchOrd'.
type Pattern s = Seq (Matcher s)

-- | A (chunked) input string to 'match' on a 'Pattern' in a trie.
--
-- /Note:/ Input strings can be infinite. Since the tries are always finite,
-- an infinite input string is only consumed until either a match has been
-- found or the applicable paths in the trie have been exhaustively searched.
type Str s = [s]

-- | A captured chunk of an input 'Str'ing.
newtype Capture s = Capture { captured :: s }
    deriving stock (Eq, Ord, Show, Read, Generic)
    deriving anyclass NFData

-- | A 'Matcher' is applied on a single chunk of an input 'Str'ing
-- while looking for a 'match' and either /succeeds/ or /fails/. If it succeeds,
-- it may 'Capture' the chunk.
data Matcher s
    -- | Match and capture an arbitrary chunk of an input string.
    = AnyStr
    -- | Match a chunk of an input string exactly, capturing nothing.
    | EqStr !s
    deriving stock (Eq, Show, Read, Generic)
    deriving anyclass NFData

-- | A total order for matchers that subsumes the partial order for
-- the preference between overlapping patterns on a 'matchPrefix'.
--
-- >>> MatchPrefixOrd AnyStr > MatchPrefixOrd (EqStr "a")
-- True
--
-- >>> let p1 = mempty |> EqStr "a" |> EqStr "b"
-- >>> let p2 = mempty |> AnyStr    |> EqStr "b"
-- >>> matchPrefixOrd p1 < matchPrefixOrd p2
-- True
newtype MatchPrefixOrd s = MatchPrefixOrd (Matcher s)
    deriving Eq

matchPrefixOrd :: Pattern s -> Seq (MatchPrefixOrd s)
matchPrefixOrd = coerce

-- | A total order for matchers that subsumes the partial order for
-- the preference between overlapping patterns on a 'match'.
--
-- >>> MatchOrd AnyStr < MatchOrd (EqStr "a")
-- True
--
-- >>> let p1 = mempty |> EqStr "a" |> EqStr "b"
-- >>> let p2 = mempty |> AnyStr    |> EqStr "b"
-- >>> matchOrd p1 > matchOrd p2
-- True
newtype MatchOrd s = MatchOrd (Matcher s)
    deriving Eq

matchOrd :: Pattern s -> Seq (MatchOrd s)
matchOrd = coerce

instance Ord s => Ord (MatchOrd s) where
    compare (MatchOrd m1) (MatchOrd m2) =
        compare (MatchPrefixOrd m2) (MatchPrefixOrd m1)

instance Ord s => Ord (MatchPrefixOrd s) where
    compare (MatchPrefixOrd m1) (MatchPrefixOrd m2) =
        case (m1, m2) of
            (AnyStr  ,  EqStr _) -> GT
            (EqStr  _,   AnyStr) -> LT
            (AnyStr  ,   AnyStr) -> EQ
            (EqStr s1, EqStr s2) -> compare s1 s2

-- | Check whether two patterns are overlapping, i.e. whether there
-- exists a 'Str'ing that is a (full) match for both patterns.
--
-- >>> let p1 = mempty |> EqStr "a" |> AnyStr
-- >>> let p2 = mempty |> AnyStr    |> EqStr "b"
-- >>> let p3 = mempty |> EqStr "a" |> EqStr "c"
-- >>> overlapping p1 p1
-- False
-- >>> overlapping p1 p2
-- True
-- >>> overlapping p1 p3
-- True
-- >>> overlapping p2 p3
-- False
overlapping :: Eq s => Pattern s -> Pattern s -> Bool
overlapping p1 p2 = Seq.length p1 == Seq.length p2 && go True p1 p2
  where
    go eq Empty             Empty              = not eq
    go _  Empty             _                  = False
    go _  _                 Empty              = False
    go _  (EqStr _ :<| p1') (AnyStr   :<| p2') = go False p1' p2'
    go _  (AnyStr  :<| p1') (EqStr _  :<| p2') = go False p1' p2'
    go eq (AnyStr  :<| p1') (AnyStr   :<| p2') = go eq    p1' p2'
    go eq (EqStr s :<| p1') (EqStr s' :<| p2') = s == s' && go eq p1' p2'

-- | Apply a string to a pattern, returning the unmatched
-- suffix of the pattern together with the captured chunks and the
-- remaining (unmatched) suffix of the input string.
--
-- >>> let p = mempty |> EqStr "a" |> AnyStr |> EqStr "c"
-- >>> let s = ["a", "b", "d"]
-- >>> apply s p
-- (fromList [EqStr "c"],fromList [Capture {captured = "b"}],["d"])
apply :: Eq s => Str s -> Pattern s -> (Pattern s, Seq (Capture s), Str s)
apply = go Seq.empty
  where
    go !cs ss Empty = (Empty, cs, ss)
    go !cs [] p     = (p    , cs, [])
    go !cs str@(s:s') pat@(m :<| p') = case m of
        AnyStr   -> go (cs |> Capture s) s' p'
        EqStr ss -> if s == ss
            then go cs s' p'
            else (pat, cs, str)

-- | Apply a string to a pattern, returning the captures.
--
-- >>> let p = mempty |> EqStr "a" |> AnyStr |> EqStr "c"
-- >>> let s = ["a", "b", "d"]
-- >>> applyCapture s p
-- fromList [Capture {captured = "b"}]
applyCapture :: Eq s => Str s -> Pattern s -> Seq (Capture s)
applyCapture s p = case apply s p of
    (_, c, _) -> c
{-# INLINE applyCapture #-}

-- | (Re)Construct the longest input 'Str'ing matching a prefix of a pattern,
-- using the given captures to satisfy matchers.  As long as there are enough
-- captures to satisfy all matchers in the pattern, the resulting string will
-- always be a (full) match for the pattern.
--
-- Furthermore, if an input string @s@ is a (full) match for a pattern @p@, then
--
-- @
-- unapplyCapture p (applyCapture s p) == s
-- @
--
-- >>> let p = mempty |> EqStr "a" |> AnyStr |> EqStr "c"
-- >>> let s = ["a", "b", "c"]
-- >>> unapplyCapture p (applyCapture s p)
-- ["a","b","c"]
unapplyCapture :: Pattern s -> Seq (Capture s) -> Str s
unapplyCapture = go []
  where
    go !str Empty           _          = str
    go !str (_ :|> AnyStr ) Empty      = str
    go !str (p :|> EqStr s) cs         = go (s : str) p cs
    go !str (p :|> AnyStr ) (cs :|> c) = go (captured c : str) p cs

-- | Apply a string to a pattern, returning the captures iff
-- the string is a (full) match for the pattern.
--
-- >>> let p = mempty |> EqStr "a" |> AnyStr |> EqStr "c"
-- >>> applyMatch ["a", "b", "c", "d"] p
-- Nothing
-- >>> applyMatch ["a", "b", "c"] p
-- Just (fromList [Capture {captured = "b"}])
applyMatch :: Eq s => Str s -> Pattern s -> Maybe (Seq (Capture s))
applyMatch s p = case apply s p of
    (Empty, cs, []) -> Just cs
    _               -> Nothing

-- | Apply a string to a pattern, returning 'True' iff the string
-- is a (full) match for the pattern.
applyMatches :: Eq s => Str s -> Pattern s -> Bool
applyMatches s = isJust . applyMatch s

-- | Apply a string to a pattern, returning the captures iff
-- the string is a prefix match for the pattern.
--
-- >>> let p = mempty |> EqStr "a" |> AnyStr |> EqStr "c"
-- >>> applyMatchPrefix ["a", "b", "c", "d"] p
-- Just (fromList [Capture {captured = "b"}])
applyMatchPrefix :: Eq s => Str s -> Pattern s -> Maybe (Seq (Capture s))
applyMatchPrefix s p = case apply s p of
    (Empty, cs, _) -> Just cs
    _              -> Nothing

-- | Apply a string to a pattern, returning 'True' iff the string
-- is a prefix match for the pattern.
applyMatchesPrefix:: Eq s => Str s -> Pattern s -> Bool
applyMatchesPrefix s = isJust . applyMatchPrefix s

-----------------------------------------------------------------------------
-- List conversion

-- | Create a pattern trie from a list of patterns and associated values.
--
-- \(\mathcal{O}(n \cdot k)\), where \(n\) is the length of the list and
-- \(k\) is the length of the longest pattern in the list.
fromAssocList :: (Eq s, Hashable s) => [(Pattern s, a)] -> Trie s a
fromAssocList = foldl' add mempty
  where
    add t (p, a) = insert p a t
{-# INLINE fromAssocList #-}

-- | Create a list of patterns and associated values from a pattern trie.
--
-- \(\mathcal{O}(n \cdot k)\), where \(n\) is the number of values in the trie
-- and \(k\) is the length of the longest pattern in the trie.
toAssocList :: (Eq s, Hashable s) => Trie s a -> [(Pattern s, a)]
toAssocList t = foldrWithKey (\p a l -> (p, a) : l) [] t
{-# INLINE toAssocList #-}

-----------------------------------------------------------------------------
-- Updates

-- | Insert the value for the given pattern into the trie.
--
-- \(\Theta(k)\), where \(k\) is the length of the pattern.
insert :: (Eq s, Hashable s) => Pattern s -> a -> Trie s a -> Trie s a
insert p !a = go p
  where
    go Empty            = modVal (const (Just a))
    go (AnyStr  :<| p') = modVar ((Just $!) . go p' . fromMaybe mempty)
    go (EqStr s :<| p') = modStr $ \m ->
        let t' = HashMap.lookupDefault mempty s m
        in HashMap.insert s (go p' t') m
{-# SPECIALISE insert :: BPattern -> a -> BTrie a -> BTrie a #-}
{-# SPECIALISE insert :: TPattern -> a -> TTrie a -> TTrie a #-}

-- | Update the value of the given pattern in the trie, if it exists.
--
-- \(\mathcal{O}(k)\), where \(k\) is the length of the pattern.
adjust :: (Eq s, Hashable s) => Pattern s -> (a -> a) -> Trie s a -> Trie s a
adjust p f = go p
  where
    go Empty            = modVal (f <$!>)
    go (AnyStr  :<| p') = modVar (go p' <$!>)
    go (EqStr s :<| p') = modStr (HashMap.adjust (go p') s)
{-# SPECIALISE adjust :: BPattern -> (a -> a) -> BTrie a -> BTrie a #-}
{-# SPECIALISE adjust :: TPattern -> (a -> a) -> TTrie a -> TTrie a #-}

-- | Remove the value for the given pattern from the trie, if it exists.
--
-- \(\mathcal{O}(k)\), where \(k\) is the length of the pattern.
delete :: (Eq s, Hashable s) => Pattern s -> Trie s a -> Trie s a
delete p = go p
  where
    go Empty            = modVal (const Nothing)
    go (AnyStr  :<| p') = modVar (maybe Nothing (go' p'))
    go (EqStr s :<| p') = modStr (HashMap.update (go' p') s)

    go' p' t = case go p' t of
        t' | null t' -> Nothing
        t'           -> Just t'
{-# SPECIALISE delete :: BPattern -> BTrie a -> BTrie a #-}
{-# SPECIALISE delete :: TPattern -> TTrie a -> TTrie a #-}

-----------------------------------------------------------------------------
-- Lookups

-- $lookupBigO
-- ===Time Complexity (successful lookups)
-- \(\Theta(k)\), where \(k\) is the length of the pattern.

type LookupNextR r s a = Trie s a -> Pattern s -> r -> r

lookupIter :: (Eq s, Hashable s) => LookupNextR r s a -> r -> Pattern s -> Trie s a -> r
lookupIter nextR = go
  where
    go r p t =
        let !r' = nextR t p r
        in case p of
            Empty          -> r'
            AnyStr  :<| p' -> maybe r' (go r' p') (vartrie t)
            EqStr s :<| p' -> maybe r' (go r' p') (HashMap.lookup s (strtries t))
{-# INLINE lookupIter #-}

-- | Lookup the trie rooted at the longest prefix of a pattern, returning it
-- together with the remaining suffix of the pattern.
lookupPrefixTrie :: (Eq s, Hashable s) => Pattern s -> Trie s a -> (Trie s a, Pattern s)
lookupPrefixTrie p t = lookupIter nextR (t, Empty) p t
  where
    nextR t' p' = const (t', p')
{-# SPECIALISE lookupPrefixTrie :: BPattern -> BTrie a -> (BTrie a, BPattern) #-}
{-# SPECIALISE lookupPrefixTrie :: TPattern -> TTrie a -> (TTrie a, TPattern) #-}

-- | Lookup the value for the longest matching prefix of a pattern,
-- returning it together with the remaining suffix of the pattern.
-- If there is no value in the trie for any prefix of the given pattern,
-- the result is 'Nothing'.
lookupPrefix :: (Eq s, Hashable s) => Pattern s -> Trie s a -> Maybe (a, Pattern s)
lookupPrefix p t = lookupIter nextR Nothing p t
  where
    nextR t' p' r = ((,p') <$!> value t') <|> r
{-# SPECIALISE lookupPrefix :: BPattern -> BTrie a -> Maybe (a, BPattern) #-}
{-# SPECIALISE lookupPrefix :: TPattern -> TTrie a -> Maybe (a, TPattern) #-}

-- | Lookup the value of a pattern.
-- If there is no value in the trie for the given pattern, the result is
-- 'Nothing'.
lookup :: (Eq s, Hashable s) => Pattern s -> Trie s a -> Maybe a
lookup p t = case lookupPrefixTrie p t of
    (t', Empty) -> value t'
    _           -> Nothing
{-# INLINE lookup #-}

-----------------------------------------------------------------------------
-- Matching

-- $matchingBigO
-- ===Time Complexity (successful matches)
-- In what follows \(k\) is always the length of the input string (i.e. the
-- number of chunks).
--
-- __/Best case/__: \(\Theta(k)\), when the input string matches the most
-- specific pattern in the trie (i.e. with the least captures) from all those
-- that have a matching prefix for the string.
--
-- __/Worst case/__: \(\mathcal{O}(2^k)\), when there are \(2^{k-1}\) distinct
-- patterns of length at least \(k\) in the trie, all of which have a prefix of
-- length \(k-1\) that is a prefix match for the input string, but none except
-- for the most general of them are an actual (full) match. This is a
-- pathological case that comes about from backtracking to more general patterns
-- and is illustrated in an example with \(k=3\) for the input string
-- @["a","a","b"]@ below.
--
-- <<docs-extra/backtrack.png>>
--
-- Nodes with values are filled, choice points are blue, dead ends are red and
-- dashed lines indicate backtracking.  The above trie contains the keys
-- (patterns)
--
--   * @mempty |> EqStr "a" |> EqStr "a" |> EqStr "a"@
--   * @mempty |> EqStr "a" |> AnyStr    |> EqStr "a"@
--   * @mempty |> AnyStr    |> EqStr "a" |> EqStr "a"@
--   * @mempty |> AnyStr    |> AnyStr    |> EqStr "b"@
--
-- with some arbitrary values. The paths are explored left-to-right in a
-- depth-first search. The number of steps for a match in the worst case is more
-- accurately approximated by
-- \[ \underbrace{2^k - 1}_{\text{downwards searching}} + \underbrace{2^{k-1} - 1}_{\text{upwards backtracking}} \]
-- Dropping the asymptotically insignificant constants and lower terms yields the
-- bound.  For realistic values of \(k\), however, the difference matters.

-- | A choice point for backtracking to alternative branches.
data Choice s a = Choice !(Seq (Capture s)) (Str s) !(Trie s a)

type MatchNextR r s a = Trie s a -> Seq (Capture s) -> Str s -> r -> r

matchIter :: (Eq s, Hashable s) => MatchNextR r s a -> r -> Str s -> Trie s a -> r
matchIter nextR = go Seq.empty []
  where
    go !cs !cps r str t =
        let !r' = nextR t cs str r
        in case str of
            []     -> if isJust (value t) then r' else backtrack r' cps
            (s:s') -> case HashMap.lookup s (strtries t) of
                Just t' -> case vartrie t of
                    Nothing  -> go cs cps r' s' t'
                    Just t'' ->
                        -- Add a new choice point
                        let cps' = Choice (cs |> Capture s) s' t'' : cps
                        in go cs cps' r' s' t'
                Nothing -> case vartrie t of
                    Just t' -> go (cs |> Capture s) cps r' s' t'
                    Nothing -> backtrack r' cps

    -- Continue at the last choice point, if any
    backtrack r [] = r
    backtrack r (Choice cs' ss' t' : cps') = go cs' cps' r ss' t'
{-# INLINE matchIter #-}

-- | Lookup the trie rooted at the longest matching prefix of the input string,
-- returning it together with any captured parts and the remaining (unmatched)
-- suffix of the input string.
--
-- In particular, if the input string is a (full) match for a pattern, the
-- returned trie is the subtrie that is rooted at the associated 'value'.
matchPrefixTrie :: (Eq s, Hashable s) => Str s -> Trie s a -> (Trie s a, Seq (Capture s), Str s)
matchPrefixTrie s t = matchIter nextR (t, Seq.empty, []) s t
  where
    nextR t' cs' s' = const (t', cs', s')
{-# SPECIALISE matchPrefixTrie :: BStr -> BTrie a -> (BTrie a, Seq BCapture, BStr) #-}
{-# SPECIALISE matchPrefixTrie :: TStr -> TTrie a -> (TTrie a, Seq TCapture, TStr) #-}

-- | Lookup the value for the longest matching prefix of the input string,
-- returning it together with any captured parts and the remaining
-- (unmatched) suffix of the input string. If no prefix of the input
-- string matches any pattern in the trie, the result is 'Nothing'.
matchPrefix :: (Eq s, Hashable s) => Str s -> Trie s a -> Maybe (a, Seq (Capture s), Str s)
matchPrefix s t = matchIter nextR Nothing s t
  where
    nextR t' cs s' r = ((,cs,s') <$!> value t') <|> r
{-# SPECIALISE matchPrefix :: BStr -> BTrie a -> Maybe (a, Seq BCapture, BStr) #-}
{-# SPECIALISE matchPrefix :: TStr -> TTrie a -> Maybe (a, Seq TCapture, TStr) #-}

-- | Lookup the value for an input string by matching it against the patterns of
-- a trie. The value of the matching pattern, if any, is returned together with
-- any captured parts of the input string. If the input string does not match
-- any pattern in the trie, the result is 'Nothing'.
--
match :: (Eq s, Hashable s) => Str s -> Trie s a -> Maybe (a, Seq (Capture s))
match s t = case matchPrefixTrie s t of
    (t', cs, []) -> (,cs) <$!> value t'
    _            -> Nothing
{-# INLINE match #-}

-----------------------------------------------------------------------------
-- Folds and traversals with keys (patterns)

traverseWithKey :: Applicative f => (Pattern s -> a -> f b) -> Trie s a -> f (Trie s b)
traverseWithKey f t = go mempty t
  where
    go !p (Trie vals vars v) =
        let f1 = HashMap.traverseWithKey (\k -> go (p |> EqStr k)) vals
            f2 = traverse (go (p |> AnyStr)) vars
            f3 = traverse (f p) v
        in Trie <$> f1 <*> f2 <*> f3

foldMapWithKey :: Monoid m => (Pattern s -> a -> m) -> Trie s a -> m
foldMapWithKey f = getConst . traverseWithKey (\p -> Const . f p)

foldrWithKey :: (Pattern s -> a -> b -> b) -> b -> Trie s a -> b
foldrWithKey f b t = appEndo (foldMapWithKey (\p -> Endo . f p) t) b

-----------------------------------------------------------------------------
-- Utilities

modStr :: (HashMap s (Trie s a) -> HashMap s (Trie s a)) ->  Trie s a -> Trie s a
modStr f t = t { strtries = f (strtries t) }
{-# INLINE modStr #-}

modVar :: (Maybe (Trie s a) -> Maybe (Trie s a)) -> Trie s a -> Trie s a
modVar f t = t { vartrie = f (vartrie t) }
{-# INLINE modVar #-}

modVal :: (Maybe a -> Maybe a) -> Trie s a -> Trie s a
modVal f t = t { value = f (value t) }
{-# INLINE modVal #-}

type BTrie a = Trie ByteString a
type BStr = Str ByteString
type BCapture = Capture ByteString
type BPattern = Pattern ByteString

type TTrie a = Trie Text a
type TStr = Str Text
type TCapture = Capture Text
type TPattern = Pattern Text