module System.Console.Docopt.ParseUtils
    (
        module System.Console.Docopt.ParseUtils,
        module System.Console.Docopt.ApplicativeParsec,
        module Data.Char,
    )
    where

import System.Console.Docopt.ApplicativeParsec

import           Data.Map (Map)
import qualified Data.Map as M

import Data.Char (isSpace, toUpper, toLower)

-- * Constants

lowers, uppers, letters, numerics, specialChars, alphanumerics, alphanumSpecial :: String
lowers = ['a'..'z']
uppers = ['A'..'Z']
letters = lowers++uppers
numerics = ['0'..'9']++"-_"
specialChars = " :/"
alphanumerics = letters++numerics
alphanumSpecial = alphanumerics ++ specialChars


-- * Basic Parsers

caseInsensitive :: String -> CharParser u String
caseInsensitive = mapM (\c -> char (toLower c) <|> char (toUpper c))

lookAhead_ :: CharParser u a -> CharParser u ()
lookAhead_ p = lookAhead p >> return ()

isNotFollowedBy :: Show a => CharParser u a -> CharParser u Bool
isNotFollowedBy p = option False (notFollowedBy p >> return True)

isInlineSpace :: Char -> Bool
isInlineSpace c = notElem c "\n\r"
                   && isSpace c

inlineSpace :: CharParser u Char
inlineSpace = satisfy isInlineSpace
            <?> "inline-space"

-- | like `spaces`, except does not match newlines
inlineSpaces :: CharParser u ()
inlineSpaces = skipMany (satisfy isInlineSpace)
             <?> "inline-spaces"

inlineSpaces1 :: CharParser u ()
inlineSpaces1 = skipMany1 (satisfy isInlineSpace)
              <?> "1+ inline-spaces"

spaces1 :: CharParser u ()
spaces1 = skipMany1 (satisfy isSpace)
        <?> ">=1 spaces"

endline :: CharParser u Char
endline = inlineSpaces >> newline

optionalEndline :: CharParser u ()
optionalEndline = inlineSpaces >> optional newline

pipe :: CharParser u Char
pipe = char '|' <?> "'|'"

ellipsis :: CharParser u String
ellipsis = inlineSpaces >> string "..."
         <?> "'...'"

manyTill1 :: CharParser u a -> CharParser u b -> CharParser u [a]
manyTill1 p end = do
  first <- p
  rest <- manyTill p end
  return $ first : rest

-- |@skipUntil p@ ignores everything that comes before @p@.
-- Returns what @p@ returns.
skipUntil :: Show a => CharParser u a -> CharParser u ()
skipUntil p = skipMany (notFollowedBy p >> anyChar)

pGroup :: Char -> CharParser u a -> Char -> CharParser u [a]
pGroup beg elemParser end = between (char beg) (inlineSpaces >> char end)
                            $ (inlineSpaces >> notFollowedBy pipe >> elemParser)
                              `sepBy`
                              (inlineSpaces >> pipe)

betweenS :: String -> String -> CharParser u a -> CharParser u [a]
betweenS b e p = between begin end manyP
                 where begin = try $ string b
                       end = try $ inlineSpaces >> string e
                       manyP = p `sepBy` inlineSpaces1


-- | Data.Map utils
alterAllWithKey :: Ord k => (k -> Maybe a -> Maybe a) -> [k] -> Map k a -> Map k a
alterAllWithKey f ks m = foldl (\m' k -> M.alter (f k) k m') m ks
