-- | Combinators that are useful for building command-line
-- parsers. These build off the functions in
-- "System.Console.MultiArg.Prim". Unlike those functions, these
-- functions have no access to the internals of the parser.
module System.Console.MultiArg.Combinator (
  -- * Parser combinators
  option,
  optionMaybe,
  notFollowedBy,
  
  -- * Short options
  shortNoArg,
  shortOptionalArg,
  shortOneArg,
  shortTwoArg,
  shortVariableArg,

  -- * Long options
  nonGNUexactLongOpt,
  matchApproxLongOpt,
  matchNonGNUApproxLongOpt,
  longNoArg,
  longOptionalArg,
  longOneArg,
  longTwoArg,
  longVariableArg,
  
  -- * Mixed options
  mixedNoArg,
  mixedOptionalArg,
  mixedOneArg,
  mixedTwoArg,
  mixedVariableArg,
  
  -- * Other words
  matchApproxWord ) where
  
import Data.Text ( Text, isPrefixOf )
import Data.Set ( Set )
import qualified Data.Set as Set
import Control.Monad ( liftM )

import System.Console.MultiArg.Prim
  ( ParserT, throw, try, approxLongOpt,
    nextArg, pendingShortOptArg, nonOptionPosArg,
    pendingShortOpt, nonPendingShortOpt,
    exactLongOpt, nextArg, (<?>))
import System.Console.MultiArg.Option
  ( LongOpt, ShortOpt )
import qualified System.Console.MultiArg.Error as E
import System.Console.MultiArg.Error
  ( Error, parseErr )
import Control.Applicative ((<|>), many)
import Control.Monad ( void )
import Data.Monoid ( mconcat )

-- | @option x p@ runs parser p. If p fails without consuming any
-- input, returns x. Otherwise, returns p.
option :: (Error e, Monad m) =>
          a
          -> ParserT s e m a
          -> ParserT s e m a
option x p = p <|> return x

-- | @optionMaybe p@ runs parser p. If p fails without returning any
-- input, returns Nothing. If p succeeds, returns the result of p
-- wrapped in a Just. If p fails but consumes input, optionMaybe
-- fails.
optionMaybe :: (Error e, Monad m)
               => ParserT s e m a
               -> ParserT s e m (Maybe a)
optionMaybe p = option Nothing (liftM Just p)

-- | @notFollowedBy p@ succeeds only if parser p fails. If p fails,
-- notFollowedBy succeeds without consuming any input. If p succeeds
-- and consumes input, notFollowedBy fails and consumes input. If p
-- succeeds and does not consume any input, notFollowedBy fails and
-- does not consume any input.
notFollowedBy :: (Error e, Monad m)
                 => ParserT s e m a
                 -> ParserT s e m ()
notFollowedBy p =
  void $ ((try p >> throw (E.parseErr E.ExpNotFollowedBy E.SawFollowedBy))
          <|> return ())


-- | Parses only a non-GNU style long option (that is, one that does
-- not take option arguments by attaching them with an equal sign,
-- such as @--lines=20@).
nonGNUexactLongOpt :: (Error e, Monad m)
                      => LongOpt
                      -> ParserT s e m LongOpt
nonGNUexactLongOpt l = try $ do
  (lo, maybeArg) <- exactLongOpt l
  case maybeArg of
    Nothing -> return lo
    (Just t) ->
      throw (parseErr (E.ExpNonGNUExactLong l)
            (E.SawGNULongOptArg t))

-- | Takes a long option and a set of long options. If the next word
-- on the command line unambiguously starts with the name of the long
-- option given, returns the actual text found on the command line,
-- the long option, and the text of any GNU-style option
-- argument. Make sure that the long option you are looking for is
-- both the first argument and that it is included in the set;
-- otherwise this parser will always fail.
matchApproxLongOpt :: (Error e, Monad m)
                      => LongOpt
                      -> Set LongOpt
                      -> ParserT s e m (Text, LongOpt, Maybe Text)
matchApproxLongOpt l s = try $ do
  a@(t, lo, _) <- approxLongOpt s
  if lo == l
    then return a
    else throw (parseErr (E.ExpMatchingApproxLong l s)
               (E.SawNotMatchingApproxLong t lo))

-- | Like matchApproxLongOpt but only parses non-GNU-style long
-- options.
matchNonGNUApproxLongOpt :: (Error e, Monad m)
                            => LongOpt
                            -> Set LongOpt
                            -> ParserT s e m (Text, LongOpt)
matchNonGNUApproxLongOpt l s = try $ do
  (t, lo, arg) <- matchApproxLongOpt l s
  let err b = throw (parseErr (E.ExpNonGNUMatchingApproxLong l s)
                    (E.SawMatchingApproxLongWithArg b))
  maybe (return (t, lo)) err arg

-- | Examines the possible words in Set. If there are no pendings,
-- then get the next word and see if it matches one of the words in
-- Set. If so, returns the word actually parsed and the matching word
-- from Set. If there is no match, fails without consuming any input.
matchApproxWord :: (Error e, Monad m)
                   => Set Text
                   -> ParserT s e m (Text, Text)
matchApproxWord s = try $ do
  a <- nextArg
  let p t = a `isPrefixOf` t
      matches = Set.filter p s
      err saw = throw (parseErr (E.ExpApproxWord s) saw)
  case Set.toList matches of
    [] -> err (E.SawNoMatches a)
    (x:[]) -> return (a, x)
    _ -> err (E.SawMultipleApproxMatches matches a)

-- | Parses short options that do not take any argument. (It is
-- however okay for the short option to be combined with other short
-- options in the same word.)
shortNoArg :: (Error e, Monad m)
            => ShortOpt
            -> ParserT s e m ShortOpt
shortNoArg s = pendingShortOpt s <|> nonPendingShortOpt s

-- | Parses short options that take an optional argument. The argument
-- can be combined in the same word with the short option (@-c42@) or
-- can be in the ext word (@-c 42@).
shortOptionalArg :: (Error e, Monad m)
                 => ShortOpt
                 -> ParserT s e m (ShortOpt, Maybe Text)
shortOptionalArg s = do
  so <- shortNoArg s
  a <- optionMaybe (pendingShortOptArg <|> nonOptionPosArg)
  return (so, a)

-- | Parses short options that take a required argument.  The argument
-- can be combined in the same word with the short option (@-c42@) or
-- can be in the ext word (@-c 42@).
shortOneArg :: (Error e, Monad m) =>
               ShortOpt
               -> ParserT s e m (ShortOpt, Text)
shortOneArg s = do
  so <- shortNoArg s
  a <- pendingShortOptArg <|> nextArg
  return (so, a)

-- | Parses short options that take two required arguments. The first
-- argument can be combined in the same word with the short option
-- (@-c42@) or can be in the ext word (@-c 42@). The next argument
-- will have to be in a separate word.
shortTwoArg :: (Error e, Monad m)
               => ShortOpt
               -> ParserT s e m (ShortOpt, Text, Text)
shortTwoArg s = do
  (so, a1) <- shortOneArg s
  a2 <- nextArg
  return (so, a1, a2)

-- | Parses short options that take a variable number of
-- arguments. This will keep on parsing option arguments until it
-- encounters one that does not "look like" an option--that is, until
-- it encounters one that begins with a dash. Therefore, the only way
-- to terminate a variable argument option if it is the last option is
-- with a stopper. The first argument can be combined in the same word
-- with the short option (@-c42@) or can be in the ext word (@-c
-- 42@). Subsequent arguments will have to be in separate words.
shortVariableArg :: (Error e, Monad m)
                 => ShortOpt
                 -> ParserT s e m (ShortOpt, [Text])
shortVariableArg s = do
  so <- shortNoArg s
  firstArg <- optionMaybe pendingShortOptArg
  rest <- many nonOptionPosArg
  let result = maybe rest ( : rest ) firstArg
  return (so, result)

-- | Parses long options that do not take any argument.
longNoArg :: (Error e, Monad m)
           => LongOpt
           -> ParserT s e m LongOpt
longNoArg = nonGNUexactLongOpt

-- | Parses long options that take a single, optional argument. The
-- single argument can be given GNU-style (@--lines=20@) or non-GNU
-- style in separate words (@lines 20@).
longOptionalArg :: (Error e, Monad m)
                   => LongOpt
                   -> ParserT s e m (LongOpt, Maybe Text)
longOptionalArg = exactLongOpt

-- | Parses long options that take a single, required argument. The
-- single argument can be given GNU-style (@--lines=20@) or non-GNU
-- style in separate words (@lines 20@).
longOneArg :: (Error e, Monad m)
                 => LongOpt
                 -> ParserT s e m (LongOpt, Text)
longOneArg l = do
  (lo, mt) <- longOptionalArg l
  case mt of
    (Just t) -> return (lo, t)
    Nothing -> do
      a <- nextArg <?> E.parseErr E.ExpLongOptArg E.SawNoArgsLeft
      return (l, a)

-- | Parses long options that take a double, required argument. The
-- first argument can be given GNU-style (@--lines=20@) or non-GNU
-- style in separate words (@lines 20@). The second argument will have
-- to be in a separate word.
longTwoArg :: (Error e, Monad m)
                 => LongOpt
                 -> ParserT s e m (LongOpt, Text, Text)
longTwoArg l = do
  (lo, mt) <- longOptionalArg l
  case mt of
    (Just t) -> do
      a2 <- nextArg
      return (lo, t, a2)
    Nothing -> do
      a1 <- nextArg
      a2 <- nextArg
      return (lo, a1, a2)

-- | Parses long options that take a variable number of
-- arguments. This will keep on parsing option arguments until it
-- encounters one that does not "look like" an option--that is, until
-- it encounters one that begins with a dash. Therefore, the only way
-- to terminate a variable argument option if it is the last option is
-- with a stopper. The first argument can be combined in the same word
-- with the short option (@--lines=20@) or can be in the ext word
-- (@--lines 42@). Subsequent arguments will have to be in separate
-- words.
longVariableArg :: (Error e, Monad m)
                   => LongOpt
                   -> ParserT s e m (LongOpt, [Text])
longVariableArg l = do
  (lo, mt) <- longOptionalArg l
  rest <- many nonOptionPosArg
  return (lo, maybe rest (:rest) mt)

-- | Parses at least one long option and a variable number of short
-- and long options that take no arguments.
mixedNoArg :: (Error e, Monad m)
              => LongOpt
              -> [LongOpt]
              -> [ShortOpt]
              -> ParserT s e m (Either ShortOpt LongOpt)
mixedNoArg l ls ss = mconcat ([f] ++ longs ++ shorts) where
  toLong lo = do
    r <- longNoArg lo
    return $ Right r
  toShort so = do
    s <- shortNoArg so
    return $ Left s
  f = toLong l
  longs = map toLong ls
  shorts = map toShort ss

-- | Parses at least one long option and a variable number of short
-- and long options that take an optional argument.
mixedOptionalArg ::
  (Error e, Monad m)
  => LongOpt
  -> [LongOpt]
  -> [ShortOpt]
  -> ParserT s e m ((Either ShortOpt LongOpt), Maybe Text)
mixedOptionalArg l ls ss = mconcat ([f] ++ longs ++ shorts) where
  toLong lo = do
    (o, a) <- longOptionalArg lo
    return $ (Right o, a)
  toShort so = do
    (o, a) <- shortOptionalArg so
    return $ (Left o, a)
  f = toLong l
  longs = map toLong ls
  shorts = map toShort ss

-- | Parses at least one long option and additional long and short
-- options that take one argument.
mixedOneArg ::
  (Error e, Monad m)
  => LongOpt
  -> [LongOpt]
  -> [ShortOpt]
  -> ParserT s e m ((Either ShortOpt LongOpt), Text)
mixedOneArg l ls ss = mconcat ([f] ++ longs ++ shorts) where
  toLong lo = do
    (o, a) <- longOneArg lo
    return (Right o, a)
  toShort lo = do
    (o, a) <- shortOneArg lo
    return (Left o, a)
  f = toLong l
  longs = map toLong ls
  shorts = map toShort ss

-- | Parses at least one long option and additonal long and short
-- options that take two arguments.
mixedTwoArg ::
  (Error e, Monad m)
  => LongOpt
  -> [LongOpt]
  -> [ShortOpt]
  -> ParserT s e m ((Either ShortOpt LongOpt), Text, Text)
mixedTwoArg l ls ss = mconcat ([f] ++ longs ++ shorts) where
  toLong lo = do
    (o, a1, a2) <- longTwoArg lo
    return (Right o, a1, a2)
  toShort lo = do
    (o, a1, a2) <- shortTwoArg lo
    return (Left o, a1, a2)
  f = toLong l
  longs = map toLong ls
  shorts = map toShort ss

-- | Parses at least one long option and additional long and short
-- options that take a variable number of arguments.
mixedVariableArg ::
  (Error e, Monad m)
  => LongOpt
  -> [LongOpt]
  -> [ShortOpt]
  -> ParserT s e m ((Either ShortOpt LongOpt), [Text])
mixedVariableArg l ls ss = mconcat ([f] ++ longs ++ shorts) where
  toLong lo = do
    (o, a) <- longVariableArg lo
    return (Right o, a)
  toShort lo = do
    (o, a) <- shortVariableArg lo
    return (Left o, a)
  f = toLong l
  longs = map toLong ls
  shorts = map toShort ss