{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}

module WithCli (
  withCli,
  WithCli(),
  HasArguments(argumentsParser),
  atomicArgumentsParser,
  Argument(argumentType, parseArgument),
  -- * Modifiers
  withCliModified,
  Modifier(..),
  -- * Useful Re-exports
  SOP.Generic,
  SOP.HasDatatypeInfo,
  SOP.Code,
  SOP.All2,
  Typeable,
  Proxy(..),
  ) where

-- todo: add pure api with withCli

import           Data.Proxy
import           Data.Typeable
import qualified Generics.SOP as SOP
import           System.Environment

import           System.Console.GetOpt.Generics.Modifier
import           WithCli.Argument
import           WithCli.HasArguments
import           WithCli.Parser
import           WithCli.Result

-- | 'withCli' converts an IO operation into a program with a proper CLI.
--   Retrieves command line arguments through 'withArgs'.
--   @main@ (the given IO operation) can have arbitrarily many parameters
--   provided all parameters have instances for 'HasArguments'.
--
--   May throw the following exceptions:
--
--   - @'ExitFailure' 1@ in case of invalid options. Error messages are written
--     to @stderr@.
--   - @'ExitSuccess'@ in case @--help@ is given. (@'ExitSuccess'@ behaves like
--     a normal exception, except that -- if uncaught -- the process will exit
--     with exit-code @0@.) Help output is written to @stdout@.
--
--   Example:

-- ### Start "docs/Simple.hs" "module Simple where\n\n" Haddock ###

-- |
-- >  import WithCli
-- >
-- >  main :: IO ()
-- >  main = withCli run
-- >
-- >  run :: String -> Int -> Bool -> IO ()
-- >  run s i b = print (s, i, b)

-- ### End ###

-- | Using the above program in a shell:

-- ### Start "docs/Simple.shell-protocol" "" Haddock ###

-- |
-- >  $ program foo 42 true
-- >  ("foo",42,True)
-- >  $ program --help
-- >  program [OPTIONS] STRING INTEGER BOOL
-- >    -h  --help  show help and exit
-- >  $ program foo 42 bar
-- >  cannot parse as BOOL: bar
-- >  # exit-code 1
-- >  $ program
-- >  missing argument of type STRING
-- >  missing argument of type INTEGER
-- >  missing argument of type BOOL
-- >  # exit-code 1
-- >  $ program foo 42 yes bar
-- >  unknown argument: bar
-- >  # exit-code 1

-- ### End ###

withCli :: forall main . WithCli main => main -> IO ()
withCli = withCliModified []

-- | This is a variant of 'withCli' that allows to tweak the generated
--   command line interface by providing a list of 'Modifier's.
withCliModified :: forall main . WithCli main => [Modifier] -> main -> IO ()
withCliModified mods main = do
  args <- getArgs
  modifiers <- handleResult (mkModifiers mods)
  _run modifiers (return $ emptyParser ()) (\ () -> main) args

-- | Everything that can be used as a @main@ function with 'withCli' needs to
--   have an instance of 'WithCli'. You shouldn't need to implement your own
--   instances.
class WithCli main where
  _run :: Modifiers -> Result (Parser Unnormalized a) -> (a -> main) -> [String] -> IO ()

instance WithCli (IO ()) where
  _run modifiers mkParser mkMain args = do
    progName <- getProgName
    parser <- handleResult mkParser
    a <- handleResult $ runParser progName modifiers
      (normalizeParser (applyModifiers modifiers parser)) args
    mkMain a

instance (HasArguments a, WithCli rest) => WithCli (a -> rest) where
  _run modifiers fa mkMain args =
    _run modifiers (combine fa (argumentsParser modifiers Nothing)) (\ (a, r) -> mkMain a r) args