-- | Functions and types used by the "Multiarg" module.  You don't
-- have to worry about \"breaking\" anything by using this module.
-- This module is separate from "Multiarg" only because it makes the
-- documentation in that module cleaner, as that module should satisfy
-- most use cases.  Use this module if you want more control over
-- error handling, or if you want to process arguments using pure
-- functions rather than IO functions.
module Multiarg.Internal where

import Multiarg.Maddash
import Multiarg.Limeline
import Multiarg.Types
import Multiarg.Util
import Data.Either (partitionEithers)
import System.Environment
import System.Exit
import qualified System.IO as IO

limelineOutputToParsedCommandLine
  :: ([Either [Output a] (PosArg a)], Maybe OptName)
  -> ParsedCommandLine a
limelineOutputToParsedCommandLine (ls, mayOpt) =
  ParsedCommandLine (concatMap f ls) mayOpt
  where
    f ei = case ei of
      Left os -> map g os
        where
          g o = case o of
            Good a -> Right a
            OptionError oe -> Left oe
      Right (PosArg pa) -> [Right pa]


-- | Indicates the result of parsing a command line.
data ParsedCommandLine a
  = ParsedCommandLine [Either OptionError a] (Maybe OptName)
  -- ^ @ParsedCommandLine a b@, where:
  --
  -- @a@ is a list of errors and results, in the original order in
  -- which they appeared on the command line.
  --
  -- @b@ is @Just p@ if the user included an /option/ at the end of
  -- the command line and there were not enough following /words/ to
  -- provide the /option/ with its necessary /option arguments/, where
  -- @p@ is the /name/ of the /option/ with insufficient
  -- /option arguments/;
  -- otherwise 'Nothing'.
  deriving (Eq, Ord, Show)

instance Functor ParsedCommandLine where
  fmap f (ParsedCommandLine ls m) = ParsedCommandLine
    (map (fmap f) ls) m

-- | Gets the results from a parsed command line.  If there were
-- errors, returns a 'Left' with an error message; otherwise, returns
-- a 'Right' with a list of the results.
parsedResults
  :: ParsedCommandLine a
  -> Either (String, [String]) [a]
parsedResults (ParsedCommandLine ls mayOpt) =
  let (ers, gds) = partitionEithers ls
  in case (ers, mayOpt) of
      ([], Nothing) -> Right gds
      ([], Just opt) -> Left (insufficientOptArgs opt, [])
      (x:xs, Just opt) -> Left
        (optError x, map optError xs ++ [insufficientOptArgs opt])
      (x:xs, Nothing) -> Left
        (optError x, map optError xs)

insufficientOptArgs :: OptName -> String
insufficientOptArgs n = "not enough arguments given for option: "
  ++ optNameToString n

optError :: OptionError -> String
optError oe = case oe of
  BadOption opt ->
    "unrecognized option: " ++ optNameToString opt
  LongArgumentForZeroArgumentOption lng arg ->
    "argument given for option that takes no arguments. "
    ++ "option: --" ++ longNameToString lng
    ++ " argument: " ++ optArgToString arg


-- | Parses a command line; a pure function (unlike
-- 'parseCommandLineIO').
parseCommandLinePure

  :: [OptSpec a]
  -- ^ All program options

  -> (String -> a)
  -- ^ Processes non-option positional arguments

  -> [String]
  -- ^ Input tokens from the command line, probably obtained from
  -- 'getArgs'

  -> ParsedCommandLine a

parseCommandLinePure os fPos inp =
  limelineOutputToParsedCommandLine limeOut
  where
    limeOut = interspersed shrts lngs fPos (map Word inp)
    (shrts, lngs) = splitOptSpecs os

-- | Parses a command line.  Runs in the IO monad so that it can do
-- some tedious things for you:
--
-- * fetches the /words/ on command line using 'getArgs' and the name
-- of the program with 'getProgName'
--
-- * prints help, if the user requested help, and exits
-- successfully
--
-- * prints an error message and exits unsuccessfully, if the user
-- entered a bad command line (such as an unknown option)
--
-- If you don't want this degree of automation or if you want a pure
-- function, see the 'parseCommandLinePure' function in the
-- "Multiarg.Internal" module.
parseCommandLine
  :: (String -> String)
  -- ^ Returns help for your command.  This function is applied to the
  -- name of the program being run, which is obtained from
  -- 'getProgName'.  The function should return a string that gives
  -- help for how to use your command; this string is printed as-is.

  -> [OptSpec a]
  -- ^ All program /options/.  An /option/ for @-h@ and for @--help@ is
  -- added for you, using the help function given above.  If the user
  -- asks for help, then it is printed and the program exits
  -- successfully.  If the user gives a command line with one or more
  -- errors in it, an error message is printed, along with something
  -- like @Enter program-name --help for help@.

  -> (String -> a)
  -- ^ Processes /positional arguments/.

  -> IO [a]
  -- ^ Fetches the /words/ from the command line arguments using
  -- 'getArgs' and parses them.  If there is an error, prints an error
  -- message and exits unsuccessfully.  Otherwise, returns the parsed
  -- result, where each item in the list corresponds to a parsed
  -- /option/ or /positional argument/ in the order in which it
  -- appeared on the command line.
parseCommandLine fHelp os fPos = do
  progName <- getProgName
  args <- getArgs
  case parsedResults $ parseCommandLineHelp os fPos args of
    Left (e1, es) -> do
      IO.hPutStrLn IO.stderr $ progName ++ ": error"
      _ <- mapM (IO.hPutStrLn IO.stderr) $ e1 : es
      IO.hPutStrLn IO.stderr $ "enter \"" ++ progName ++ " --help\" "
        ++ "for help."
      exitFailure
    Right mayResults -> case sequence mayResults of
      Nothing -> do
        putStr (fHelp progName)
        exitSuccess
      Just ls -> return ls


-- | Automatically adds a /short option/, @-h@, and a /long option/,
-- @--help@.  Intended primarily for use by the 'parseCommandLineIO'
-- function.
parseCommandLineHelp
  :: [OptSpec a]
  -> (String -> a)
  -> [String]
  -> ParsedCommandLine (Maybe a)
parseCommandLineHelp os fPos inp =
  limelineOutputToParsedCommandLine
  $ interspersed shrts lngs (fmap Just fPos) (map Word inp)
  where
    (shrts, lngs) = addHelpOption os