{-# LANGUAGE CPP #-}

-- | Most of client interface.
module HaskellCI.Cli where

import HaskellCI.Prelude

import System.Exit           (exitFailure)
import System.FilePath.Posix (takeFileName)
import System.IO             (hPutStrLn, stderr)

import qualified Options.Applicative as O

import HaskellCI.Config
import HaskellCI.OptparseGrammar
import HaskellCI.VersionInfo

-------------------------------------------------------------------------------
-- Command
-------------------------------------------------------------------------------

data Command
    = CommandBash FilePath
    | CommandGitHub FilePath
    | CommandRegenerate
    | CommandListGHC
    | CommandDumpConfig
    | CommandVersionInfo
  deriving Show

-------------------------------------------------------------------------------
-- Options
-------------------------------------------------------------------------------

data Options = Options
    { optOutput         :: Maybe Output
    , optConfig         :: ConfigOpt
    , optCwd            :: Maybe FilePath
    , optInputType      :: Maybe InputType
    , optConfigMorphism :: Config -> Config
    }

instance Semigroup Options where
    Options b d c e f <> Options b' d' c' e' f' =
        Options (b <|> b') (d <> d') (c <|> c') (e <|> e') (f' . f)

defaultOptions :: Options
defaultOptions = Options
    { optOutput         = Nothing
    , optConfig         = ConfigOptAuto
    , optCwd            = Nothing
    , optInputType      = Nothing
    , optConfigMorphism = id
    }

optionsWithOutputFile :: FilePath -> Options
optionsWithOutputFile fp = defaultOptions
    { optOutput = Just (OutputFile fp)
    }

data Output = OutputStdout | OutputFile FilePath

data ConfigOpt
    = ConfigOptAuto
    | ConfigOpt FilePath
    | ConfigOptNo
  deriving (Eq, Show)

instance Semigroup ConfigOpt where
    a <> ConfigOptAuto = a
    _ <> b             = b

-------------------------------------------------------------------------------
-- InputType
-------------------------------------------------------------------------------

data InputType
    = InputTypePackage -- ^ @.cabal@
    | InputTypeProject -- ^ @cabal.project
  deriving Show

optInputType' :: Options -> FilePath -> InputType
optInputType' opts path =
    fromMaybe def (optInputType opts)
  where
    def | "cabal.project" `isPrefixOf` takeFileName path = InputTypeProject
        | otherwise                                      = InputTypePackage

-------------------------------------------------------------------------------
-- Parsers
-------------------------------------------------------------------------------

optionsP :: O.Parser Options
optionsP = Options
    <$> O.optional outputP
    <*> configOptP
    <*> O.optional (O.strOption (O.long "cwd" <> O.metavar "Dir" <> O.action "directory" <> O.help "Directory to change to"))
    <*> O.optional inputTypeP
    <*> runOptparseGrammar configGrammar

configOptP :: O.Parser ConfigOpt
configOptP = file <|> noconfig <|> pure ConfigOptAuto
  where
    file = ConfigOpt <$> O.strOption (O.long "config" <> O.metavar "CONFIGFILE" <> O.action "file" <> O.help "Configuration file")
    noconfig = O.flag' ConfigOptNo (O.long "no-config" <> O.help "Don't read configuration file")

outputP :: O.Parser Output
outputP =
    OutputFile <$> O.strOption (O.long "output" <> O.short 'o' <> O.metavar "FILE" <> O.action "file" <> O.help "Output file") <|>
    O.flag' OutputStdout (O.long "stdout" <> O.help "Use stdout output")

versionP :: O.Parser (a -> a)
versionP = O.infoOption haskellCIVerStr $ mconcat
    [ O.long "version"
    , O.short 'V'
    , O.help "Print version information"
    ]

inputTypeP :: O.Parser InputType
inputTypeP = pkg <|> prj where
    pkg = O.flag' InputTypePackage $ O.long "package"
    prj = O.flag' InputTypeProject $ O.long "project"

cliParserInfo :: O.ParserInfo (Command, Options)
cliParserInfo = O.info ((,) <$> cmdP <*> optionsP O.<**> versionP O.<**> O.helper) $ mconcat
    [ O.fullDesc
    , O.header "haskell-ci - generate CI scripts for Haskell projects"
    ]
  where
    cmdP = O.subparser (mconcat
        [ O.command "regenerate"   $ O.info (pure CommandRegenerate)  $ O.progDesc "Regenerate outputs"
        , O.command "bash"         $ O.info bashP                     $ O.progDesc "Generate local-bash-docker script"
        , O.command "github"       $ O.info githubP                   $ O.progDesc "Generate GitHub Actions config"
        , O.command "list-ghc"     $ O.info (pure CommandListGHC)     $ O.progDesc "List known GHC versions"
        , O.command "dump-config"  $ O.info (pure CommandDumpConfig)  $ O.progDesc "Dump cabal.haskell-ci config with default values"
        , O.command "version-info" $ O.info (pure CommandVersionInfo) $ O.progDesc "Print versions info haskell-ci was compiled with"
        ])

    bashP = CommandBash
        <$> O.strArgument (O.metavar "CABAL.FILE" <> O.action "file" <> O.help "Either <pkg.cabal> or cabal.project")

    githubP = CommandGitHub
        <$> O.strArgument (O.metavar "CABAL.FILE" <> O.action "file" <> O.help "Either <pkg.cabal> or cabal.project")

-------------------------------------------------------------------------------
-- Parsing helpers
-------------------------------------------------------------------------------

parseOptions :: [String] -> IO (FilePath, Options)
parseOptions argv = case res of
    O.Success (cmd, opts) -> do
        path <- fromCmd cmd
        return (path, opts)
    O.Failure f -> case O.renderFailure f "haskell-ci" of
        (help, _) -> hPutStrLn stderr help >> exitFailure
    O.CompletionInvoked _ -> exitFailure -- unexpected
  where
    res = O.execParserPure (O.prefs O.subparserInline) cliParserInfo argv

    fromCmd :: Command -> IO FilePath
    fromCmd (CommandBash fp)   = return fp
    fromCmd (CommandGitHub fp) = return fp
    fromCmd cmd                = fail $ "Command without filepath: " ++ show cmd
