{- Copyright © 2012, Vincent Elisha Lee Frey.  All rights reserved.
 - This is open source software distributed under a MIT license.
 - See the file 'LICENSE' for further information.
 -}
module System.Console.CmdTheLine.Trie where

import qualified Data.Map as M

{-
 - This implementation maps any non-ambiguous prefix of a key to its value.
 -}

type CMap = M.Map Char

data Value a = Pre a -- Value bound by a prefix key.
             | Key a -- Value bound by an entire key.
             | Amb   -- No Value bound due to ambiguity in the key.
             | Nil   -- Attempt to retrieve a Value from an empty Trie.
               deriving (Eq)

data Trie a = Trie
  { val   :: Value a
  , succs :: CMap (Trie a)
  } deriving (Eq)

data LookupFail = Ambiguous | NotFound deriving (Show)

empty :: Trie a
empty = Trie Nil M.empty

isEmpty :: Eq a => Trie a -> Bool
isEmpty = (== empty)

add :: Trie a -> String -> a -> Trie a
add t k v = go t k (length k) 0 v (Pre v {- Allocate less. -})
  where
  go t k len i v preV =
    if i == len
       then Trie (Key v) (succs t)
       else Trie newVal  newSuccs
    where
    newVal = case val t of
      Amb       -> Amb
      Pre _     -> Amb
      v@(Key _) -> v
      Nil       -> preV

    newSuccs = M.insert (k !! i) (go t' k len (i + 1) v preV) (succs t)
      where
      t' = maybe empty id $ M.lookup (k !! i) (succs t)

findNode :: String -> Trie a -> Maybe (Trie a)
findNode k t = go t k (length k) 0
  where
  go t k len i =
    if i == len
       then Just t
       else goNext =<< M.lookup (k !! i) (succs t)
    where
    goNext t' = go t' k len (i + 1)


lookup :: String -> Trie a -> Either LookupFail a
lookup k t = case findNode k t of
  Nothing -> Left NotFound
  Just t' -> case val t' of
    Key v -> Right v
    Pre v -> Right v
    Amb   -> Left  Ambiguous
    Nil   -> Left  NotFound

ambiguities :: Trie a -> String -> [String]
ambiguities t pre = case findNode pre t of
  Nothing -> []
  Just t' -> case val t' of
    Amb -> go [] pre $ M.toList (succs t') : []
    _   -> []

  where
  go acc pre assocs = case assocs of
    []        -> error "saw lone empty list while searching for ambiguities"
    [[]]      -> acc
    [] : rest -> go acc (init pre) rest
    _         -> descend assocs
    where
    descend ((top : bottom) : rest) = go acc' pre' assocs'
      where
      ( c, t'' ) = top

      assocs'    = M.toList (succs t'') : bottom : rest

      pre'       = pre ++ return c
      acc'       = case val t'' of
        Key _ -> pre' : acc
        Nil   -> error "saw Nil on descent"
        _     -> acc

fromList :: [( String, a )] -> Trie a
fromList assoc = foldl consume empty assoc
  where
  consume t ( k, v ) = add t k v