{- |
    Module      :  $Header$
    Description :  Compiler options
    Copyright   :  (c) 2005        Martin Engelke
                       2007        Sebastian Fischer
                       2011 - 2016 Björn Peemöller
                       2016 - 2017 Finn Teegen
                       2018        Kai-Oliver Prott
    License     :  BSD-3-clause

    Maintainer  :  fte@informatik.uni-kiel.de
    Stability   :  experimental
    Portability :  portable

    This module defines data structures holding options for the
    compilation of Curry programs, and utility functions for printing
    help information as well as parsing the command line arguments.
-}
module CompilerOpts
  ( Options (..), CppOpts (..), PrepOpts (..), WarnOpts (..), DebugOpts (..)
  , CaseMode (..), CymakeMode (..), Verbosity (..), TargetType (..)
  , WarnFlag (..), KnownExtension (..), DumpLevel (..), dumpLevel
  , defaultOptions, defaultPrepOpts, defaultWarnOpts, defaultDebugOpts
  , getCompilerOpts, updateOpts, usage
  ) where

import           Data.List             (intercalate, nub)
import           Data.Maybe            (isJust)
import           Data.Char             (isDigit)
import qualified Data.Map    as Map    (Map, empty, insert)
import System.Console.GetOpt
import System.Environment              (getArgs, getProgName)
import System.FilePath                 ( addTrailingPathSeparator, normalise
                                       , splitSearchPath )

import Curry.Files.Filenames           (currySubdir)
import Curry.Syntax.Extension

-- -----------------------------------------------------------------------------
-- Option data structures
-- -----------------------------------------------------------------------------

-- |Compiler options
data Options = Options
  -- general
  { optMode         :: CymakeMode         -- ^ modus operandi
  , optVerbosity    :: Verbosity          -- ^ verbosity level
  -- compilation
  , optForce        :: Bool               -- ^ force (re-)compilation of target
  , optLibraryPaths :: [FilePath]         -- ^ directories to search in
                                          --   for libraries
  , optImportPaths  :: [FilePath]         -- ^ directories to search in
                                          --   for imports
  , optHtmlDir      :: Maybe FilePath     -- ^ output directory for HTML
  , optUseSubdir    :: Bool               -- ^ use subdir for output?
  , optInterface    :: Bool               -- ^ create a FlatCurry interface file?
  , optPrepOpts     :: PrepOpts           -- ^ preprocessor options
  , optWarnOpts     :: WarnOpts           -- ^ warning options
  , optTargetTypes  :: [TargetType]       -- ^ what to generate
  , optExtensions   :: [KnownExtension]   -- ^ enabled language extensions
  , optDebugOpts    :: DebugOpts          -- ^ debug options
  , optCaseMode     :: CaseMode           -- ^ case mode
  , optCppOpts      :: CppOpts            -- ^ C preprocessor options
  } deriving Show

-- |C preprocessor options
data CppOpts = CppOpts
  { cppRun         :: Bool                -- ^ run C preprocessor
  , cppDefinitions :: Map.Map String Int  -- ^ defintions for the C preprocessor
  } deriving Show

-- |Preprocessor options
data PrepOpts = PrepOpts
  { ppPreprocess :: Bool      -- ^ apply custom preprocessor
  , ppCmd        :: String    -- ^ preprocessor command
  , ppOpts       :: [String]  -- ^ preprocessor options
  } deriving Show

data CaseMode
  = CaseModeFree
  | CaseModeHaskell
  | CaseModeProlog
  | CaseModeGoedel
  deriving (Eq, Show)

-- |Warning options
data WarnOpts = WarnOpts
  { wnWarn         :: Bool       -- ^ show warnings? (legacy option)
  , wnWarnFlags    :: [WarnFlag] -- ^ Warnings flags (see below)
  , wnWarnAsError  :: Bool       -- ^ Should warnings be treated as errors?
  } deriving Show

-- |Debug options
data DebugOpts = DebugOpts
  { dbDumpLevels      :: [DumpLevel] -- ^ dump levels
  , dbDumpEnv         :: Bool        -- ^ dump compilation environment
  , dbDumpRaw         :: Bool        -- ^ dump data structure
  , dbDumpAllBindings :: Bool        -- ^ dump all bindings instead of just the
                                     --   local bindings
  , dbDumpSimple      :: Bool        -- ^ print more readable environments
  } deriving Show

-- | Default compiler options
defaultOptions :: Options
defaultOptions = Options
  { optMode         = ModeMake
  , optVerbosity    = VerbStatus
  , optForce        = False
  , optLibraryPaths = []
  , optImportPaths  = []
  , optHtmlDir      = Nothing
  , optUseSubdir    = True
  , optInterface    = True
  , optPrepOpts     = defaultPrepOpts
  , optWarnOpts     = defaultWarnOpts
  , optTargetTypes  = []
  , optExtensions   = []
  , optDebugOpts    = defaultDebugOpts
  , optCaseMode     = CaseModeFree
  , optCppOpts      = defaultCppOpts
  }

-- | Default C preprocessor options
defaultCppOpts :: CppOpts
defaultCppOpts = CppOpts
  { cppRun         = False
  , cppDefinitions = Map.empty
  }

-- | Default preprocessor options
defaultPrepOpts :: PrepOpts
defaultPrepOpts = PrepOpts
  { ppPreprocess = False
  , ppCmd        = ""
  , ppOpts       = []
  }

-- | Default warning options
defaultWarnOpts :: WarnOpts
defaultWarnOpts = WarnOpts
  { wnWarn        = True
  , wnWarnFlags   = stdWarnFlags
  , wnWarnAsError = False
  }

-- | Default dump options
defaultDebugOpts :: DebugOpts
defaultDebugOpts = DebugOpts
  { dbDumpLevels      = []
  , dbDumpEnv         = False
  , dbDumpRaw         = False
  , dbDumpAllBindings = False
  , dbDumpSimple      = False
  }

-- |Modus operandi of the program
data CymakeMode
  = ModeHelp           -- ^ Show help information and exit
  | ModeVersion        -- ^ Show version and exit
  | ModeNumericVersion -- ^ Show numeric version, suitable for later processing
  | ModeMake           -- ^ Compile with dependencies
  deriving (Eq, Show)

-- |Verbosity level
data Verbosity
  = VerbQuiet  -- ^ be quiet
  | VerbStatus -- ^ show status of compilation
  deriving (Eq, Ord, Show)

-- |Description and flag of verbosities
verbosities :: [(Verbosity, String, String)]
verbosities = [ ( VerbQuiet , "0", "quiet" )
              , ( VerbStatus, "1", "status")
              ]

-- |Type of the target file
data TargetType
  = Tokens               -- ^ Source code tokens
  | Comments             -- ^ Source code comments
  | Parsed               -- ^ Parsed source code
  | FlatCurry            -- ^ FlatCurry
  | TypedFlatCurry       -- ^ Typed FlatCurry
  | TypeAnnotatedFlatCurry -- ^ Type-annotated FlatCurry
  | AbstractCurry          -- ^ AbstractCurry
  | UntypedAbstractCurry   -- ^ Untyped AbstractCurry
  | Html                   -- ^ HTML documentation
  | AST                  -- ^ Abstract-Syntax-Tree after checks
  | ShortAST             -- ^ Abstract-Syntax-Tree with shortened decls
    deriving (Eq, Show)

-- |Warnings flags
data WarnFlag
  = WarnMultipleImports      -- ^ Warn for multiple imports
  | WarnDisjoinedRules       -- ^ Warn for disjoined function rules
  | WarnUnusedGlobalBindings -- ^ Warn for unused global bindings
  | WarnUnusedBindings       -- ^ Warn for unused local bindings
  | WarnNameShadowing        -- ^ Warn for name shadowing
  | WarnOverlapping          -- ^ Warn for overlapping rules/alternatives
  | WarnIncompletePatterns   -- ^ Warn for incomplete pattern matching
  | WarnMissingSignatures    -- ^ Warn for missing type signatures
  | WarnMissingMethods       -- ^ Warn for missing method implementations
  | WarnOrphanInstances      -- ^ Warn for orphan instances
  | WarnIrregularCaseMode
    deriving (Eq, Bounded, Enum, Show)

-- |Warning flags enabled by default
stdWarnFlags :: [WarnFlag]
stdWarnFlags =
  [ WarnMultipleImports   , WarnDisjoinedRules   --, WarnUnusedGlobalBindings
  , WarnUnusedBindings    , WarnNameShadowing    , WarnOverlapping
  , WarnIncompletePatterns, WarnMissingSignatures, WarnMissingMethods
  , WarnIrregularCaseMode
  ]

-- |Description and flag of warnings flags
warnFlags :: [(WarnFlag, String, String)]
warnFlags =
  [ ( WarnMultipleImports     , "multiple-imports"
    , "multiple imports"               )
  , ( WarnDisjoinedRules      , "disjoined-rules"
    , "disjoined function rules"       )
  , ( WarnUnusedGlobalBindings, "unused-global-bindings"
    , "unused bindings"                )
  , ( WarnUnusedBindings      , "unused-bindings"
    , "unused bindings"                )
  , ( WarnNameShadowing       , "name-shadowing"
    , "name shadowing"                 )
  , ( WarnOverlapping         , "overlapping"
    , "overlapping function rules"     )
  , ( WarnIncompletePatterns  , "incomplete-patterns"
    , "incomplete pattern matching"    )
  , ( WarnMissingSignatures   , "missing-signatures"
    , "missing type signatures"        )
  , ( WarnMissingMethods      , "missing-methods"
    , "missing method implementations" )
  , ( WarnOrphanInstances     , "orphan-instances"
    , "orphan instances"               )
  , ( WarnIrregularCaseMode   , "irregular-case-mode"
    , "irregular case mode")
  ]

-- |Dump level
data DumpLevel
  = DumpCondCompiled      -- ^ dump source code after conditional compiling
  | DumpParsed            -- ^ dump source code after parsing
  | DumpExtensionChecked  -- ^ dump source code after extension checking
  | DumpTypeSyntaxChecked -- ^ dump source code after type syntax checking
  | DumpKindChecked       -- ^ dump source code after kind checking
  | DumpSyntaxChecked     -- ^ dump source code after syntax checking
  | DumpPrecChecked       -- ^ dump source code after precedence checking
  | DumpDeriveChecked     -- ^ dump source code after derive checking
  | DumpInstanceChecked   -- ^ dump source code after instance checking
  | DumpTypeChecked       -- ^ dump source code after type checking
  | DumpExportChecked     -- ^ dump source code after export checking
  | DumpQualified         -- ^ dump source code after qualification
  | DumpDerived           -- ^ dump source code after deriving
  | DumpDesugared         -- ^ dump source code after desugaring
  | DumpDictionaries      -- ^ dump source code after dictionary transformation
  | DumpNewtypes          -- ^ dump source code after removing newtype constructors
  | DumpSimplified        -- ^ dump source code after simplification
  | DumpLifted            -- ^ dump source code after lambda-lifting
  | DumpTranslated        -- ^ dump IL code after translation
  | DumpCaseCompleted     -- ^ dump IL code after case completion
  | DumpTypedFlatCurry    -- ^ dump typed FlatCurry code
  | DumpFlatCurry         -- ^ dump FlatCurry code
    deriving (Eq, Bounded, Enum, Show)

-- |Description and flag of dump levels
dumpLevel :: [(DumpLevel, String, String)]
dumpLevel = [ (DumpCondCompiled     , "dump-cond" , "conditional compiling"           )
            , (DumpParsed           , "dump-parse", "parsing"                         )
            , (DumpExtensionChecked , "dump-exc"  , "extension checking"              )
            , (DumpTypeSyntaxChecked, "dump-tsc"  , "type syntax checking"            )
            , (DumpKindChecked      , "dump-kc"   , "kind checking"                   )
            , (DumpSyntaxChecked    , "dump-sc"   , "syntax checking"                 )
            , (DumpPrecChecked      , "dump-pc"   , "precedence checking"             )
            , (DumpDeriveChecked    , "dump-dc"   , "derive checking"                 )
            , (DumpInstanceChecked  , "dump-inc"  , "instance checking"               )
            , (DumpTypeChecked      , "dump-tc"   , "type checking"                   )
            , (DumpExportChecked    , "dump-ec"   , "export checking"                 )
            , (DumpQualified        , "dump-qual" , "qualification"                   )
            , (DumpDerived          , "dump-deriv", "deriving"                        )
            , (DumpDesugared        , "dump-ds"   , "desugaring"                      )
            , (DumpDictionaries     , "dump-dict" , "dictionary insertion"            )
            , (DumpNewtypes         , "dump-new"  , "removing newtype constructors"   )
            , (DumpLifted           , "dump-lift" , "lifting"                         )
            , (DumpSimplified       , "dump-simpl", "simplification"                  )
            , (DumpTranslated       , "dump-trans", "pattern matching compilation"    )
            , (DumpCaseCompleted    , "dump-cc"   , "case completion"                 )
            , (DumpTypedFlatCurry   , "dump-tflat", "translation into typed FlatCurry")
            , (DumpFlatCurry        , "dump-flat" , "translation into FlatCurry"      )
            ]

-- |Description and flag of language extensions
extensions :: [(KnownExtension, String, String)]
extensions =
  [ ( AnonFreeVars             , "AnonFreeVars"
    , "enable anonymous free variables"              )
  , ( CPP                      , "CPP"
    , "run C preprocessor"                           )
  , ( ExistentialQuantification, "ExistentialQuantification"
    , "enable existentially quantified types"        )
  , ( FunctionalPatterns       , "FunctionalPatterns"
    , "enable functional patterns"                   )
  , ( NegativeLiterals         , "NegativeLiterals"
    , "desugar negated literals as negative literal" )
  , ( NoImplicitPrelude        , "NoImplicitPrelude"
    , "do not implicitly import the Prelude"         )
  ]

-- -----------------------------------------------------------------------------
-- Parsing of the command line options.
--
-- Because some flags require additional arguments, the structure is slightly
-- more complicated to enable malformed arguments to be reported.
-- -----------------------------------------------------------------------------

-- |Instead of just returning the resulting 'Options' structure, we also
-- collect errors from arguments passed to specific options.
type OptErr = (Options, [String])

-- |An 'OptErrTable' consists of a list of entries of the following form:
--   * a flag to be recognized on the command line
--   * an explanation text for the usage information
--   * a modification funtion adjusting the options structure
-- The type is parametric about the option's type to adjust.
type OptErrTable opt = [(String, String, opt -> opt)]

onOpts :: (Options -> Options) -> OptErr -> OptErr
onOpts f (opts, errs) = (f opts, errs)

onCppOpts :: (CppOpts -> CppOpts) -> OptErr -> OptErr
onCppOpts f (opts, errs) = (opts { optCppOpts = f (optCppOpts opts) }, errs)

onPrepOpts :: (PrepOpts -> PrepOpts) -> OptErr -> OptErr
onPrepOpts f (opts, errs) = (opts { optPrepOpts = f (optPrepOpts opts) }, errs)

onWarnOpts :: (WarnOpts -> WarnOpts) -> OptErr -> OptErr
onWarnOpts f (opts, errs) = (opts { optWarnOpts = f (optWarnOpts opts) }, errs)

onDebugOpts :: (DebugOpts -> DebugOpts) -> OptErr -> OptErr
onDebugOpts f (opts, errs)
  = (opts { optDebugOpts = f (optDebugOpts opts) }, errs)

withArg :: ((a -> b) -> OptErr -> OptErr)
        -> (String -> a -> b) -> String -> OptErr -> OptErr
withArg lift f arg = lift (f arg)

addErr :: String -> OptErr -> OptErr
addErr err (opts, errs) = (opts, errs ++ [err])

mkOptDescr :: ((opt -> opt) -> OptErr -> OptErr)
           -> String -> [String] -> String -> String -> OptErrTable opt
           -> OptDescr (OptErr -> OptErr)
mkOptDescr lift flags longFlags arg what tbl = Option flags longFlags
  (ReqArg (parseOptErr lift what tbl) arg)
  ("set " ++ what ++ " `" ++ arg ++ "', where `" ++ arg ++ "' is one of\n"
    ++ renderOptErrTable tbl)

parseOptErr :: ((opt -> opt) -> OptErr -> OptErr)
            -> String -> OptErrTable opt -> String -> OptErr -> OptErr
parseOptErr lift what table opt = case lookup3 opt table of
  Just f  -> lift f
  Nothing -> addErr $ "unrecognized " ++ what ++ '`' : opt ++ "'\n"
  where
  lookup3 _ []                  = Nothing
  lookup3 k ((k', _, v2) : kvs)
    | k == k'                   = Just v2
    | otherwise                 = lookup3 k kvs

renderOptErrTable :: OptErrTable opt -> String
renderOptErrTable ds
  = intercalate "\n" $ map (\(k, d, _) -> "  " ++ rpad maxLen k ++ ": " ++ d) ds
  where
  maxLen = maximum $ map (\(k, _, _) -> length k) ds
  rpad n x = x ++ replicate (n - length x) ' '

-- | All available compiler options
options :: [OptDescr (OptErr -> OptErr)]
options =
  -- modus operandi
  [ Option "h?" ["help"]
      (NoArg (onOpts $ \ opts -> opts { optMode = ModeHelp }))
      "display this help and exit"
  , Option "V"  ["version"]
      (NoArg (onOpts $ \ opts -> opts { optMode = ModeVersion }))
      "show the version number and exit"
  , Option ""   ["numeric-version"]
      (NoArg (onOpts $ \ opts -> opts { optMode = ModeNumericVersion }))
      "show the numeric version number and exit"
  -- verbosity
  , mkOptDescr onOpts "v" ["verbosity"] "n" "verbosity level" verbDescriptions
  , Option "q"  ["no-verb"]
      (NoArg (onOpts $ \ opts -> opts { optVerbosity = VerbQuiet } ))
      "set verbosity level to quiet"
  -- compilation
  , Option "f"  ["force"]
      (NoArg (onOpts $ \ opts -> opts { optForce = True }))
      "force compilation of target file"
  , Option "P"  ["lib-dir"]
      (ReqArg (withArg onOpts $ \ arg opts -> opts { optLibraryPaths =
        nub $ optLibraryPaths opts ++ splitSearchPath arg}) "dir[:dir]")
      "search for libraries in dir[:dir]"
  , Option "i"  ["import-dir"]
      (ReqArg (withArg onOpts $ \ arg opts -> opts { optImportPaths =
        nub $ optImportPaths opts ++
              map (normalise . addTrailingPathSeparator) (splitSearchPath arg)
              }) "dir[:dir]")
      "search for imports in dir[:dir]"
  , Option []   ["htmldir"]
      (ReqArg (withArg onOpts $ \ arg opts -> opts { optHtmlDir =
        Just arg }) "dir")
      "write HTML documentation into directory `dir'"
  , Option ""   ["no-subdir"]
      (NoArg (onOpts $ \ opts -> opts { optUseSubdir = False }))
      ("disable writing to `" ++ currySubdir ++ "' subdirectory")
  , Option ""   ["no-intf"]
      (NoArg (onOpts $ \ opts -> opts { optInterface = False }))
      "do not create an interface file"
    -- legacy warning flags
  , Option ""   ["no-warn"]
      (NoArg (onWarnOpts $ \ opts -> opts { wnWarn = False }))
      "do not print warnings"
  , Option ""   ["no-overlap-warn"]
      (NoArg (onWarnOpts $ \ opts -> opts {wnWarnFlags =
        addFlag WarnOverlapping (wnWarnFlags opts) }))
      "do not print warnings for overlapping rules"
  -- target types
  , targetOption Tokens                 "tokens"
      "generate token stream"
  , targetOption Comments               "comments"
      "generate comments stream"
  , targetOption Parsed                 "parse-only"
      "generate source representation"
  , targetOption FlatCurry              "flat"
      "generate FlatCurry code"
  , targetOption TypedFlatCurry         "typed-flat"
      "generate typed FlatCurry code"
  , targetOption TypeAnnotatedFlatCurry "type-annotated-flat"
      "generate type-annotated FlatCurry code"
  , targetOption AbstractCurry          "acy"
      "generate typed AbstractCurry"
  , targetOption UntypedAbstractCurry   "uacy"
      "generate untyped AbstractCurry"
  , targetOption Html                   "html"
      "generate html documentation"
  , targetOption AST                    "ast"
      "generate abstract syntax tree"
  , targetOption ShortAST               "short-ast"
      "generate shortened abstract syntax tree for documentation"
  , Option "F"  []
      (NoArg (onPrepOpts $ \ opts -> opts { ppPreprocess = True }))
      "use custom preprocessor"
  , Option ""   ["pgmF"]
      (ReqArg (withArg onPrepOpts $ \ arg opts -> opts { ppCmd = arg})
        "cmd")
      "execute preprocessor command <cmd>"
  , Option ""   ["optF"]
      (ReqArg (withArg onPrepOpts $ \ arg opts ->
        opts { ppOpts = ppOpts opts ++ [arg]}) "option")
      "execute preprocessor with option <option>"
  -- extensions
  , Option "e"  ["extended"]
      (NoArg (onOpts $ \ opts -> opts { optExtensions =
        nub $ kielExtensions ++ optExtensions opts }))
      "enable extended Curry functionalities"
  , mkOptDescr onOpts      "c" ["case-mode"] "mode" "case mode"          caseModeDescriptions
  , mkOptDescr onOpts      "X" []            "ext"  "language extension" extDescriptions
  , mkOptDescr onWarnOpts  "W" []            "opt"  "warning option"     warnDescriptions
  , mkOptDescr onDebugOpts "d" []            "opt"  "debug option"       debugDescriptions
  , Option ""   ["cpp"]
      (NoArg (onCppOpts $ \ opts -> opts { cppRun = True }))
      "run C preprocessor"
  , Option "D"  []
      (ReqArg (withArg ($) parseCppDefinition) "s=v")
      "define symbol `s` with value `v` for the C preprocessor"
  ]

parseCppDefinition :: String -> OptErr -> OptErr
parseCppDefinition arg optErr
  | not (null s) && not (null v) && all isDigit v
  = onCppOpts (addCppDefinition s v) optErr
  | otherwise
  = addErr (cppDefinitionErr arg) optErr
  where (s, v) = fmap (drop 1) $ break ('=' ==) arg

addCppDefinition :: String -> String -> CppOpts -> CppOpts
addCppDefinition s v opts =
  opts { cppDefinitions = Map.insert s (read v) (cppDefinitions opts) }

cppDefinitionErr :: String -> String
cppDefinitionErr = (++) "Invalid format for option '-D': "

targetOption :: TargetType -> String -> String -> OptDescr (OptErr -> OptErr)
targetOption ty flag desc
  = Option "" [flag] (NoArg (onOpts $ \ opts -> opts { optTargetTypes =
      nub $ ty : optTargetTypes opts })) desc

verbDescriptions :: OptErrTable Options
verbDescriptions = map toDescr verbosities
  where
  toDescr (flag, name, desc)
    = (name, desc, \ opts -> opts { optVerbosity = flag })

extDescriptions :: OptErrTable Options
extDescriptions = map toDescr extensions
  where
  toDescr (flag, name, desc)
    = (name, desc,
        \opts -> let cppOpts = optCppOpts opts
                 in opts { optCppOpts    =
                             cppOpts { cppRun = cppRun cppOpts || flag == CPP }
                         , optExtensions = addFlag flag (optExtensions opts)
                         })


caseModeDescriptions :: OptErrTable Options
caseModeDescriptions
  = [ ( "free"   , "use free case mode"
        , \ opts -> opts { optCaseMode = CaseModeFree    } )
    , ( "haskell", "use haskell style case mode"
        , \ opts -> opts { optCaseMode = CaseModeHaskell } )
    , ( "prolog" , "use prolog style case mode"
        , \ opts -> opts { optCaseMode = CaseModeProlog  } )
    , ( "goedel"  , "use goedel case mode"
        , \ opts -> opts { optCaseMode = CaseModeGoedel  } )
    ]

warnDescriptions :: OptErrTable WarnOpts
warnDescriptions
  = [ ( "all"  , "turn on all warnings"
        , \ opts -> opts { wnWarnFlags = [minBound .. maxBound] } )
    , ("none" , "turn off all warnings"
        , \ opts -> opts { wnWarnFlags = []                     } )
    , ("error", "treat warnings as errors"
        , \ opts -> opts { wnWarnAsError = True                 } )
    ] ++ map turnOn warnFlags ++ map turnOff warnFlags
  where
  turnOn (flag, name, desc)
    = (name, "warn for " ++ desc
      , \ opts -> opts { wnWarnFlags = addFlag flag (wnWarnFlags opts)})
  turnOff (flag, name, desc)
    = ("no-" ++ name, "do not warn for " ++ desc
      , \ opts -> opts { wnWarnFlags = removeFlag flag (wnWarnFlags opts)})

debugDescriptions :: OptErrTable DebugOpts
debugDescriptions =
  [ ( "dump-all"          , "dump everything"
    , \ opts -> opts { dbDumpLevels = [minBound .. maxBound]    })
  , ( "dump-none"         , "dump nothing"
    , \ opts -> opts { dbDumpLevels = []                        })
  , ( "dump-env"          , "additionally dump compiler environment"
    , \ opts -> opts { dbDumpEnv = True                         })
  , ( "dump-raw"          , "dump as raw AST (instead of pretty printing)"
    , \ opts -> opts { dbDumpRaw = True                         })
  , ( "dump-all-bindings" , "when dumping bindings, dump all instead of just local ones"
    , \ opts -> opts { dbDumpAllBindings = True                 })
  , ( "dump-simple" , "print a simplified, more readable environment"
    , \ opts -> opts { dbDumpSimple = True                      })

  ] ++ map toDescr dumpLevel
  where
  toDescr (flag, name, desc)
    = (name , "dump code after " ++ desc
        , \ opts -> opts { dbDumpLevels = addFlag flag (dbDumpLevels opts)})

addFlag :: Eq a => a -> [a] -> [a]
addFlag o opts = nub $ o : opts

removeFlag :: Eq a => a -> [a] -> [a]
removeFlag o opts = filter (/= o) opts

-- |Update the 'Options' record by the parsed and processed arguments
updateOpts :: Options -> [String] -> (Options, [String], [String])
updateOpts opts args = (opts', files, errs ++ errs2 ++ checkOpts opts files)
  where
  (opts', errs2) = foldl (flip ($)) (opts, []) optErrs
  (optErrs, files, errs) = getOpt Permute options args

-- |Parse the command line arguments
parseOpts :: [String] -> (Options, [String], [String])
parseOpts = updateOpts defaultOptions

-- |Check options and files and return a list of error messages
checkOpts :: Options -> [String] -> [String]
checkOpts opts _
  = [ "The option '--htmldir' is only valid for HTML generation mode"
    | isJust (optHtmlDir opts) && Html `notElem` optTargetTypes opts ]

-- |Print the usage information of the command line tool.
usage :: String -> String
usage prog = usageInfo header options
  where header = "usage: " ++ prog ++ " [OPTION] ... MODULES ..."

-- |Retrieve the compiler 'Options'
getCompilerOpts :: IO (String, Options, [String], [String])
getCompilerOpts = do
  args <- getArgs
  prog <- getProgName
  let (opts, files, errs) = parseOpts args
  return (prog, opts, files, errs)