{-# LANGUAGE ScopedTypeVariables, GADTs, MultiParamTypeClasses, FunctionalDependencies,
             FlexibleInstances, UndecidableInstances, OverlappingInstances, DeriveDataTypeable,
             FlexibleContexts, TupleSections, Rank2Types #-}
module System.Console.CmdLib.Command where

import System.Console.CmdLib.Attribute
import System.Console.CmdLib.Flag
import System.Console.GetOpt
import Data.Typeable
import Data.Either ( partitionEithers )
import Data.List( sort, intercalate, isPrefixOf )
import Data.Char( toLower )
import Data.Maybe( fromJust, isNothing )
import Control.Monad( when, forM_, void )
import Control.Exception ( evaluate, catch, SomeException )
import System.IO( hPutStrLn, stderr )
import System.Exit
import Prelude hiding ( catch )

-- | How to process options for a command. See "optionStyle" for details.
data OptionStyle = Permuted | NonPermuted | NoOptions deriving Eq

-- | A class that describes a single (sub)command. The @cmd@ type parameter is
-- just for dispatch (and the default command name is derived from this type's
-- name, but this can be overriden). It could be an empty data decl as far as
-- this library is concerned, although you may choose to store information in
-- it.
--
-- To parse the commandline for a given command, see "execute". The basic usage
-- can look something like this:
--
-- > data Flag = Summary | Unified Bool | LookForAdds Bool
-- > instance ADTFlag Flag
-- >
-- > [...]
-- >
-- > data Whatsnew = Whatsnew deriving Typeable
-- >
-- > instance Command Whatsnew (ADT Flag) where
-- >  options _ =  enable <% Summary +% Unified +% LookForAdds
-- >  summary _ = "Create a patch from unrecorded changes."
-- >
-- >  run _ f opts = do putStrLn $ "Record."
-- >                    putStrLn $ "Options: " ++ show f
-- >                    putStrLn $ "Non-options: " ++ show opts

class (Typeable cmd, FlagType flag) => Command cmd flag | cmd -> flag where
  -- | An "Attribute" mapping for flags provided by the @flag@ type parameter.
  options :: cmd -> AttributeMap Key
  options _ = EmptyMap

  -- | Set this to True if the command is a supercommand (i.e. expects another
  -- subcommand). Defaults to False. Supercommands can come with their own
  -- options, which need to appear between the supercommand and its
  -- subcommand. Any later options go to the subcommand. The "run" (and
  -- "description") method of a supercommand should use "dispatch" and
  -- "helpCommands" respectively (on its list of subcommands) itself.
  supercommand :: cmd -> Bool
  supercommand _ = False

  -- | How to process options for this command. "NoOptions" disables option
  -- processing completely and all arguments are passed in the [String]
  -- parameter to "run". "Permuted" collects everything that looks like an
  -- option (starts with a dash) and processes it. The non-option arguments are
  -- filtered and passed to run like above. Finally, "NonPermuted" only
  -- processes options until a first non-option argument is encountered. The
  -- remaining arguments are passed unchanged to run.
  optionStyle :: cmd -> OptionStyle
  optionStyle _ = Permuted

  -- | The handler that actually runs the command. Gets the @setup@ value as
  -- folded from the processed options (see "Combine") and a list of non-option
  -- arguments.
  run :: cmd -> Folded flag -> [String] -> IO ()
  run cmd _ _ = die $ "BUG: Command " ++ cmdname cmd ++ " not implemented."

  -- | Provides the commands' short synopsis.
  synopsis :: cmd -> String
  synopsis c = unwords $ cmdname c : opts where
    opts = usageDescr $ attrFun (cmdattrs c %% flag_attrs)

  -- | Provides a short (one-line) description of the command. Used in help
  -- output.
  summary :: cmd -> String
  summary _ = ""

  help :: cmd -> String
  help _ = ""

  -- | The name of the command. Normally derived automatically from @cmd@, but
  -- may be overriden.
  cmdname :: cmd -> String
  cmdname c = map toLower $ reverse . takeWhile (/= '.') . reverse . show $ typeOf c

  -- | A convenience "undefined" of the command, for use with "Commands".
  cmd :: cmd
  cmd = undefined

  cmd_flag_empty :: cmd -> Folded flag
  cmd_flag_empty _ = flag_empty (undefined :: flag)

cmdoptions :: (Command cmd flag) => cmd -> Key -> [Attribute]
cmdoptions = attrFun . options

cmdattrs :: forall cmd f . Command cmd f => cmd -> f -> [Attribute]
cmdattrs cmd = cmdoptions cmd . flag_attrkey

helpOptions :: forall cmd f. Command cmd f => cmd -> String
helpOptions cmd = unlines $ [ x | x <- [summary cmd, syn cmd, help cmd], not $ null x ] ++ opts
    where syn c | null (synopsis c) = ""
                | otherwise = "Usage: " ++ synopsis c
          opts | null opts' = []
               | otherwise = "" : map (uncurry usageInfo) opts'
          opts' = [ (grp ++ ":", getopt) |
                    (grp, getopt@(_:_)) <- helpDescr MergeSuffix $ attrFun (cmdattrs cmd %% flag_attrs) ]

helpCommands x = concatMap one x
  where one (CommandWrap c) = "    " ++ pad (cmdname c) ++ "  " ++ summary c ++ "\n"
        one (CommandGroup name l) = "\n" ++ name ++ ":\n" ++ helpCommands l
        pad str = take 15 (str ++ replicate 15 ' ') ++ " "

-- | This could be used to implement a disambiguation function
--
-- Note that there isn't presently a notion of hidden commands,
-- but we're taking them into account now for future API stability
commandNames :: Bool -- ^ show hidden commands too
             -> [CommandWrap] -> [String]
commandNames _ x = concatMap one x
  where one (CommandWrap c) = [cmdname c]
        one (CommandGroup _ cs) = concatMap one cs

execute' :: forall cmd f. (Command cmd f) => cmd -> [String] -> IO (Maybe (Folded f, [String]))
execute' cmd opts
  | "--help" `elem` opts && not (supercommand cmd) = printHelp cmd >> return Nothing
  | ("--help":_) <- opts = printHelp cmd >> return Nothing
  | NoOptions <- optionStyle cmd = return $ Just (foldr ($) (cmd_flag_empty cmd) defaults, opts)
  | otherwise = case errs' of
                     [] -> do checkFlags (flags ++ defaults)
                                [f | f <- flag_list,
                                     enabled $ attrs f,
                                     not $ nonoption attrs f]
                              sequence_ [ setglobal (attrs f) (flag_value f flags') | f <- flag_list ]
                              evaluate $ Just (flags', opts'filtered)
                     _ -> die (concat errs') >> return Nothing
  where getopts = optDescr attrs
        checkFlags providedFlags allFlags =
          forM_ allFlags $ \f -> checkFlag f fs
         where
            fs = foldr ($) (cmd_flag_empty cmd) providedFlags
            checkFlag f fs =
              (flag_eval f fs
              `catch`
              \(_ :: RequiredArgException) ->
                die $ "Missing required flag: " ++ flagDescr f)
              `catch`
              \(e :: SomeException) -> die $ "Malformed flag: " ++ show e

        order = if supercommand cmd || (optionStyle cmd == NonPermuted) then RequireOrder else Permute
        (flags, opts', errs) = getOpt order getopts opts
        flags' = foldr ($) (cmd_flag_empty cmd) (positions'' ++ extras ++ reverse flags ++ defaults)
        extras :: [Folded f -> Folded f]
        extras = [ if extra (attrs f) then flag_set f opts'filtered else id
                 | f :: f <- flag_list, enabled $ attrs f ]
        positions = [ (f, n, r) | f <- flag_list,
                                  enabled $ attrs f,
                                  Just n <- [positional $ attrs f],
                                  r <- [required $ attrs f] ]
        positions' = [ if length opts' > n
                         then Right $ flag_parse f (opts' !! n)
                         else Left (f, r)
                     | (f, n, r) <- positions ]
        (missingErrs, positions'') = filterRequiredMissing $
          partitionEithers positions'
        filterRequiredMissing (ls, rs) = (, rs) $ case filter snd ls of
          [] -> []
          rFlags -> let missingArgs = map (requiredFlagStr . fst) rFlags in
            [unwords $ "Missing required argument(s):" : missingArgs ]

        flagDescr f = let optDescr = head $ flagToOptDescr MergeSuffix attrs f
                          (ss', ls') = formatOptDescrOptions optDescr in
          intercalate "|" $ ss' ++ ls'

        errs' = errs ++ missingErrs
        opts'filtered = removemany opts'
          (reverse $ sort $ map (\(_,s,_) -> s) positions)
        removemany = foldl remove
        remove l n = let (a, b) = splitAt n l in (a ++ drop 1 b)
        defaults :: [Folded f -> Folded f]
        defaults = flag_defaults attrs
        attrs :: f -> [Attribute]
        attrs = attrFun (cmdattrs cmd %% flag_attrs)

-- | Parse options for and execute a single command (see "Command"). May be
-- useful for programs that do not need command-based "dispatch", but still
-- make use of the "Command" class to describe themselves. Handles @--help@
-- internally. You can use this as the entrypoint if your application is
-- non-modal (i.e. it has no subcommands).
execute :: forall cmd f. (Command cmd f) => cmd -> [String] -> IO ()
execute cmd opts = execute' cmd opts >>= \f -> case f of
  Just (flags, opts') -> run cmd flags opts'
  Nothing -> return ()

class Commands a where
  toCommands :: a -> [CommandWrap]

data CommandWrap where
  CommandWrap :: (Command a f, Typeable (Folded f)) => a -> CommandWrap
  CommandGroup :: String -> [CommandWrap] -> CommandWrap

instance Commands CommandWrap where
  toCommands = (:[])

instance Commands [CommandWrap] where
  toCommands = id

instance (Command c f, Typeable (Folded f)) => Commands c where
  toCommands c = [CommandWrap c]

-- | Chain commands into a list suitable for "dispatch" and "helpCommands". E.g.:
--
-- > dispatch (Command1 %: Command2 %: Command3) opts
(%:) :: (Commands a, Commands b) => a -> b -> [CommandWrap]
a %: b = toCommands a ++ toCommands b

commandGroup :: (Commands a) => String -> a -> [CommandWrap]
commandGroup s l = [CommandGroup s (toCommands l)]

-- TODO: disambiguation, hidden commands (aliases)
findCommand :: String -> [CommandWrap] -> [CommandWrap]
findCommand key list = case accum list of Left x -> [x]
                                          Right x -> x
  where accum (c@(CommandWrap cmd):comms)
          | key == cmdname cmd = Left c -- short-circuit an exact match
          | key `isPrefixOf` cmdname cmd = (c:) `fmap` accum comms
          | otherwise = accum comms
        accum (CommandGroup _ comms:comms')
          | Right c <- accum comms = (c++) `fmap` accum comms'
          | Left x <- accum comms = Left x
          | otherwise = accum comms'
        accum [] = return []

printHelp c = putStr $ helpOptions c
printCommands comms = putStr $ helpCommands comms

dispatch' :: (String -> IO ()) -- ^ fail
          -> [DispatchOpt] -> [CommandWrap] -> [String] -> IO (Maybe (CommandWrap, [String]))
dispatch' diefn dopt comms' args = case args of
  [] | isNothing def -> dieHelp "Command required."
     | otherwise -> return $ Just (fromJust def, [])
  ("--help":x) -> dispatch' diefn dopt comms' ("help":x)
  (cmd:args') -> case findCommand cmd comms of
    [] -> case def of
      Nothing -> dieHelp $ "No such command '" ++ cmd ++ "'"
      Just x -> return $ Just (x, args)
    [x] -> return $ Just (x, args')
    disamb -> dieAmb cmd disamb >> return Nothing
  where comms | null [ () | NoHelp <- dopt ] = HelpCommand comms %: comms'
              | otherwise = comms'
        dieHelp msg = printCommands comms >> diefn msg >> return Nothing
        dieAmb cmd matches =
          do putStrLn $ "The following commands match your request:"
             printCommands matches
             diefn $ "Ambiguous command '" ++ cmd ++ "'"
        def | (DefaultCommand n:_) <- [ o | o@(DefaultCommand _) <- dopt ] = Just n
            | otherwise = Nothing

data DispatchOpt = NoHelp | DefaultCommand CommandWrap
noHelp = NoHelp
defaultCommand :: (Command f x, Typeable (Folded x)) => f -> DispatchOpt
defaultCommand = DefaultCommand . CommandWrap

-- | Given a list of commands (see @"%:"@) and a list of commandline arguments,
-- dispatch on the command name, parse the commandline options (see "execute")
-- and transfer control to the command.  This function also implements the
-- @help@ pseudocommand.
dispatch :: [DispatchOpt] -> [CommandWrap] -> [String] -> IO ()
dispatch = dispatchOr die

-- | Like 'dispatch' but with the ability to control what happens when there
--   is an error on user input
dispatchOr :: (String -> IO ()) -- ^ eg. 'die'
           -> [DispatchOpt] -> [CommandWrap] -> [String] -> IO ()
dispatchOr die dopt comms opts = dispatch' die dopt comms opts >>= \c -> case c of
  Nothing -> return ()
  Just (CommandWrap c, opts') -> execute c opts'


-- | Helper for dying with an error message (nicely, at least compared to
-- "fail" in IO).
die :: String -> IO a
die msg = do hPutStrLn stderr ("FATAL: " ++ trim msg)
             exitWith (ExitFailure 1)
             return (error "unreachable")
  where trim msg | last msg == '\n' = trim $ init msg
                 | otherwise = msg

data HelpCommand = HelpCommand [CommandWrap] deriving Typeable

instance Command HelpCommand () where
  cmdname   _ = "help"
  synopsis  _ = "help [command]"
  summary   _ = "show help for a command or commands overview"
  run (HelpCommand comms) _ args = case args of
    [] -> printCommands comms
    (cmd:_) -> case findCommand cmd comms of
      [] -> printCommands comms >> die ("No such command '" ++ cmd ++ "'")
      [CommandWrap comm] -> printHelp comm
      x -> printCommands x >> die ("Ambiguous command '" ++ cmd ++ "'")