    {-# LANGUAGE TemplateHaskell, ScopedTypeVariables, DeriveDataTypeable #-}
    {-|

    Command-line options for the hledger program, and related utilities.

    -}

    module Hledger.Cli.Options (

      -- * cmdargs modes & flags
      -- | These tell cmdargs how to parse the command line arguments.
      -- There's one mode for each internal subcommand, plus a main mode.
      mainmode,
      activitymode,
      addmode,
      balancemode,
      balancesheetmode,
      cashflowmode,
      incomestatementmode,
      printmode,
      registermode,
      statsmode,
      testmode,
      convertmode,
      defCommandMode,
      argsFlag,
      helpflags,
      inputflags,
      reportflags,
      generalflagsgroup1,
      generalflagsgroup2,
      generalflagsgroup3,

      -- * raw options
      -- | To allow the cmdargs modes to be reused and extended by other
      -- packages (eg, add-ons which want to mimic the standard hledger
      -- options), we parse the command-line arguments to a simple
      -- association list, not a fixed ADT.
      RawOpts,
      inRawOpts,
      boolopt,
      intopt,
      maybeintopt,
      stringopt,
      maybestringopt,
      listofstringopt,
      setopt,
      setboolopt,

      -- * CLI options
      -- | Raw options are converted to a more convenient,
      -- package-specific options structure. This is the \"opts\" used
      -- throughout hledger CLI code.
      CliOpts(..),
      defcliopts,

      -- * CLI option accessors
      -- | Some options require more processing. Possibly these should be merged into argsToCliOpts.
      aliasesFromOpts,
      formatFromOpts,
      journalFilePathFromOpts,
      rulesFilePathFromOpts,
      OutputWidth(..),
      Width(..),
      defaultWidth,
      defaultWidthWithFlag,
      widthFromOpts,

      -- * utilities
      getHledgerAddonCommands,
      argsToCliOpts,
      moveFlagsAfterCommand,
      decodeRawOpts,
      checkCliOpts,
      rawOptsToCliOpts,
      optserror,
      showModeHelp,
      debugArgs,
      getCliOpts,

      -- * tests
      tests_Hledger_Cli_Options

    )
    where

    import qualified Control.Exception as C
    -- import Control.Monad (filterM)
    import Control.Monad (when)
    import Data.Char (isDigit)
    import Data.List
    import Data.List.Split
    import Data.Maybe
    import Data.Time.Calendar
    import Safe
    import System.Console.CmdArgs
    import System.Console.CmdArgs.Explicit
    import System.Console.CmdArgs.Text
    import System.Directory
    import System.Environment
    import System.Exit
    import Test.HUnit
    import Text.ParserCombinators.Parsec as P
    import Text.Printf

    import Hledger
    import Hledger.Data.FormatStrings as Format
    import Hledger.Cli.Version

    --
    -- 1. cmdargs mode and flag (option) definitions for the hledger CLI,
    -- can be reused by other packages as well.
    --

    -- | Our cmdargs modes parse arguments into an association list for better reuse.
    type RawOpts = [(String,String)]

    -- common flags and flag groups

    -- | Common help flags: --help, --debug, --version...
    helpflags = [
      flagNone ["help","h","?"] (setboolopt "help") "Display general help or (with --help after COMMAND) command help."
     -- ,flagNone ["browse-args"] (setboolopt "browse-args") "use a web UI to select options and build up a command line"
     ,flagOpt "1" ["debug"] (\s opts -> Right $ setopt "debug" s opts) "N" "Show debug output (optional argument sets debug level)"
     ,flagNone ["version"] (setboolopt "version") "Print version information"
     ]

    -- | Common input-related flags: --file, --rules-file, --alias...
    inputflags = [
      flagReq ["file","f"]  (\s opts -> Right $ setopt "file" s opts) "FILE" "use a different journal file; - means stdin"
     ,flagReq ["rules-file"]  (\s opts -> Right $ setopt "rules-file" s opts) "RULESFILE" "conversion rules for CSV (default: FILE.rules)"
     ,flagReq ["alias"]  (\s opts -> Right $ setopt "alias" s opts)  "ACCT=ALIAS" "convert ACCT's name to ALIAS"
     ]

    -- | Common report-related flags: --period, --cost, --display etc.
    reportflags = [
      flagReq  ["begin","b"]     (\s opts -> Right $ setopt "begin" s opts) "DATE" "report on transactions on or after this date"
     ,flagReq  ["end","e"]       (\s opts -> Right $ setopt "end" s opts) "DATE" "report on transactions before this date"
     ,flagReq  ["period","p"]    (\s opts -> Right $ setopt "period" s opts) "PERIODEXP" "report on transactions during the specified period and/or with the specified reporting interval"
     ,flagNone ["daily","D"]     (\opts -> setboolopt "daily" opts) "report by day"
     ,flagNone ["weekly","W"]    (\opts -> setboolopt "weekly" opts) "report by week"
     ,flagNone ["monthly","M"]   (\opts -> setboolopt "monthly" opts) "report by month"
     ,flagNone ["quarterly","Q"] (\opts -> setboolopt "quarterly" opts) "report by quarter"
     ,flagNone ["yearly","Y"]    (\opts -> setboolopt "yearly" opts) "report by year"
     ,flagNone ["cleared","C"]   (\opts -> setboolopt "cleared" opts) "report only on cleared transactions"
     ,flagNone ["uncleared","U"] (\opts -> setboolopt "uncleared" opts) "report only on uncleared transactions"
     ,flagNone ["cost","B"]      (\opts -> setboolopt "cost" opts) "report cost of commodities"
     ,flagReq  ["depth"]         (\s opts -> Right $ setopt "depth" s opts) "N" "hide accounts/transactions deeper than this"
     ,flagReq  ["display","d"]   (\s opts -> Right $ setopt "display" s opts) "DISPLAYEXP" "show only transactions matching the expression, which is 'dOP[DATE]' where OP is <, <=, =, >=, >"
     ,flagNone ["date2","aux-date","effective"]     (\opts -> setboolopt "date2" opts) "use transactions' secondary dates, if any"
     ,flagNone ["empty","E"]     (\opts -> setboolopt "empty" opts) "show empty/zero things which are normally elided"
     ,flagNone ["real","R"]      (\opts -> setboolopt "real" opts) "report only on real (non-virtual) transactions"
     ]

    argsFlag desc = flagArg (\s opts -> Right $ setopt "args" s opts) desc

    generalflagstitle = "\nGeneral flags"
    generalflagsgroup1 = (generalflagstitle, inputflags ++ reportflags ++ helpflags)
    generalflagsgroup2 = (generalflagstitle, inputflags ++ helpflags)
    generalflagsgroup3 = (generalflagstitle, helpflags)

    -- cmdargs modes

    -- | A basic mode template.
    defMode :: Mode RawOpts
    defMode =   Mode {
      modeNames = []
     ,modeHelp = ""
     ,modeHelpSuffix = []
     ,modeValue = []
     ,modeCheck = Right
     ,modeReform = const Nothing
     ,modeExpandAt = True
     ,modeGroupFlags = Group {
         groupNamed = []
        ,groupUnnamed = [
            flagNone ["help","h","?"] (setboolopt "help") "Display command help."
            ]
        ,groupHidden = []
        }
     ,modeArgs = ([], Nothing)
     ,modeGroupModes = toGroup []
     }

    -- | A basic subcommand mode with the given command name(s).
    defCommandMode names = defMode {
       modeNames=names
      ,modeValue=[("command", headDef "" names)]
      ,modeArgs = ([], Just $ argsFlag "[PATTERNS]")
      }

    -- | A basic subcommand mode suitable for an add-on command.
    defAddonCommandMode addon = defMode {
       modeNames = [addon]
      ,modeHelp = printf "run %s-%s" progname addon
      ,modeValue=[("command",addon)]
      ,modeGroupFlags = Group {
          groupUnnamed = []
         ,groupHidden = []
         ,groupNamed = [generalflagsgroup1]
         }
      ,modeArgs = ([], Just $ argsFlag "[ARGS]")
      }

    -- | Add command aliases to the command's help string.
    withAliases :: String -> [String] -> String
    s `withAliases` []     = s
    s `withAliases` (a:[]) = s ++ " (alias: " ++ a ++ ")"
    s `withAliases` as     = s ++ " (aliases: " ++ intercalate ", " as ++ ")"

    -- | The top-level cmdargs mode for hledger.
    mainmode addons = defMode {
      modeNames = [progname]
     ,modeHelp = unlines [
          "run the specified hledger command. Commands:"
         ]
     ,modeHelpSuffix = [""]
     ,modeArgs = ([], Just $ argsFlag "[ARGS]")
     ,modeGroupModes = Group {
        -- modes (commands) in named groups:
        groupNamed = [
          ("Adding data", [
            addmode
           ])
         ,("\nBasic reports", [
            printmode
           ,balancemode
           ,registermode
           -- ,transactionsmode
           ])
         ,("\nMore reports", [
            activitymode
           ,incomestatementmode
           ,balancesheetmode
           ,cashflowmode
           ,statsmode
           ])
         ,("\nMiscellaneous", [
            testmode
           ])
         ]
         ++ case addons of [] -> []
                           cs -> [("\nAdd-on commands found", map defAddonCommandMode cs)]
        -- modes in the unnamed group, shown first without a heading:
       ,groupUnnamed = [
         ]
        -- modes handled but not shown
       ,groupHidden = [
            convertmode
         ]
       }
     ,modeGroupFlags = Group {
         -- flags in named groups:
         groupNamed = [generalflagsgroup3]
         -- flags in the unnamed group, shown last without a heading:
        ,groupUnnamed = []
         -- flags accepted but not shown in the help:
        ,groupHidden = inputflags -- included here so they'll not raise a confusing error if present with no COMMAND
        }
     }

    -- help_postscript = [
    --   -- "DATES can be Y/M/D or smart dates like \"last month\"."
    --   -- ,"PATTERNS are regular"
    --   -- ,"expressions which filter by account name.  Prefix a pattern with desc: to"
    --   -- ,"filter by transaction description instead, prefix with not: to negate it."
    --   -- ,"When using both, not: comes last."
    --  ]

    -- visible subcommand modes

    addmode = (defCommandMode ["add"]) {
      modeHelp = "prompt for new transaction entries and add them to the journal"
     ,modeHelpSuffix = ["Defaults come from previous similar transactions; use query patterns to restrict these."]
     ,modeGroupFlags = Group {
         groupUnnamed = [
          flagNone ["no-new-accounts"]  (\opts -> setboolopt "no-new-accounts" opts) "don't allow creating new accounts"
         ]
        ,groupHidden = []
        ,groupNamed = [generalflagsgroup2]
        }
     }

    balancemode = (defCommandMode $ ["balance"] ++ aliases) {
      modeHelp = "show matched accounts and their balances" `withAliases` aliases
     ,modeGroupFlags = Group {
         groupUnnamed = [
          flagNone ["cumulative"] (\opts -> setboolopt "cumulative" opts) "with a reporting interval, show accumulated totals starting from 0"
         ,flagNone ["historical","H"] (\opts -> setboolopt "historical" opts) "with a reporting interval, show accurate historical ending balances"
         ,flagNone ["flat"] (\opts -> setboolopt "flat" opts) "show full account names, unindented"
         ,flagReq  ["drop"] (\s opts -> Right $ setopt "drop" s opts) "N" "with --flat, omit this many leading account name components"
         ,flagReq  ["format"] (\s opts -> Right $ setopt "format" s opts) "FORMATSTR" "use this custom line format"
         ,flagNone ["no-elide"] (\opts -> setboolopt "no-elide" opts) "no eliding at all, stronger than --empty"
         ,flagNone ["no-total"] (\opts -> setboolopt "no-total" opts) "don't show the final total"
         ]
        ,groupHidden = []
        ,groupNamed = [generalflagsgroup1]
        }
     }
      where aliases = ["b","bal"]

    printmode = (defCommandMode $ ["print"] ++ aliases) {
      modeHelp = "show matched journal entries" `withAliases` aliases
     ,modeGroupFlags = Group {
         groupUnnamed = []
        ,groupHidden = []
        ,groupNamed = [generalflagsgroup1]
        }
     }
      where aliases = ["p"]

    registermode = (defCommandMode $ ["register"] ++ aliases) {
      modeHelp = "show matched postings and running total" `withAliases` aliases
     ,modeGroupFlags = Group {
         groupUnnamed = [
          flagOpt (show defaultWidthWithFlag) ["width","w"] (\s opts -> Right $ setopt "width" s opts) "N" "increase or set the output width (default: 80)"
         ,flagNone ["average","A"] (\opts -> setboolopt "average" opts) "show the running average instead of the running total"
         ,flagNone ["related","r"] (\opts -> setboolopt "related" opts) "show the other postings in the transactions of those that would have been shown"
         ]
        ,groupHidden = []
        ,groupNamed = [generalflagsgroup1]
        }
     }
      where aliases = ["r","reg"]

    -- transactionsmode = (defCommandMode ["transactions"]) {
    --   modeHelp = "show matched transactions and balance in some account(s)"
    --  ,modeGroupFlags = Group {
    --      groupUnnamed = []
    --     ,groupHidden = []
    --     ,groupNamed = [generalflagsgroup1]
    --     }
    --  }

    activitymode = (defCommandMode ["activity"]) {
      modeHelp = "show a barchart of transactions per interval"
     ,modeHelpSuffix = ["The default interval is daily."]
     ,modeGroupFlags = Group {
         groupUnnamed = []
        ,groupHidden = []
        ,groupNamed = [generalflagsgroup1]
        }
     }

    incomestatementmode = (defCommandMode $ ["incomestatement"]++aliases) {
      modeHelp = "show a simple income statement" `withAliases` aliases
     ,modeGroupFlags = Group {
         groupUnnamed = []
        ,groupHidden = []
        ,groupNamed = [generalflagsgroup1]
        }
     }
      where aliases = ["is","pl"]

    balancesheetmode = (defCommandMode $ ["balancesheet"]++aliases) {
      modeHelp = "show a simple balance sheet" `withAliases` aliases
     ,modeGroupFlags = Group {
         groupUnnamed = []
        ,groupHidden = []
        ,groupNamed = [generalflagsgroup1]
        }
     }
      where aliases = ["bs"]

    cashflowmode = (defCommandMode ["cashflow","cf"]) {
      modeHelp = "show a simple cashflow statement" `withAliases` ["cf"]
     ,modeGroupFlags = Group {
         groupUnnamed = []
        ,groupHidden = []
        ,groupNamed = [generalflagsgroup1]
        }
     }

    statsmode = (defCommandMode $ ["stats"] ++ aliases) {
      modeHelp = "show quick statistics for a journal" `withAliases` aliases
     ,modeGroupFlags = Group {
         groupUnnamed = []
        ,groupHidden = []
        ,groupNamed = [generalflagsgroup1]
        }
     }
      where aliases = ["s"]

    testmode = (defCommandMode ["test"]) {
      modeHelp = "run self-tests, or just the ones matching REGEXPS"
     ,modeArgs = ([], Just $ argsFlag "[REGEXPS]")
     ,modeGroupFlags = Group {
         groupUnnamed = []
        ,groupHidden = []
        ,groupNamed = [generalflagsgroup3]
        }
     }

    -- hidden commands

    convertmode = (defCommandMode ["convert"]) {
      modeValue = [("command","convert")]
     ,modeHelp = "convert is no longer needed, just use -f FILE.csv"
     ,modeArgs = ([], Just $ argsFlag "[CSVFILE]")
     ,modeGroupFlags = Group {
         groupUnnamed = []
        ,groupHidden = helpflags
        ,groupNamed = []
        }
     }

    --
    -- 2. A package-specific data structure holding options used in this
    -- package and above, parsed from RawOpts.  This represents the
    -- command-line options that were provided, with all parsing
    -- completed, but before adding defaults or derived values (XXX add)
    --

    -- | Command line options. Used in the @hledger@ package and above.
    data CliOpts = CliOpts {
         rawopts_         :: RawOpts
        ,command_         :: String
        ,file_            :: Maybe FilePath
        ,rules_file_      :: Maybe FilePath
        ,alias_           :: [String]
        ,debug_           :: Int            -- ^ debug level, set by @--debug[=N]@. See also 'Hledger.Utils.debugLevel'.
        ,no_new_accounts_ :: Bool           -- add
        ,width_           :: Maybe String   -- register
        ,reportopts_      :: ReportOpts
     } deriving (Show, Data, Typeable)

    defcliopts = CliOpts
        def
        def
        def
        def
        def
        def
        def
        def
        def

    instance Default CliOpts where def = defcliopts

    -- | Parse raw option string values to the desired final data types.
    -- Any relative smart dates will be converted to fixed dates based on
    -- today's date. Parsing failures will raise an error.
    rawOptsToCliOpts :: RawOpts -> IO CliOpts
    rawOptsToCliOpts rawopts = do
      d <- getCurrentDay
      return defcliopts {
                  rawopts_         = rawopts
                 ,command_         = stringopt "command" rawopts
                 ,file_            = maybestringopt "file" rawopts
                 ,rules_file_      = maybestringopt "rules-file" rawopts
                 ,alias_           = map stripquotes $ listofstringopt "alias" rawopts
                 ,debug_           = intopt "debug" rawopts
                 ,no_new_accounts_ = boolopt "no-new-accounts" rawopts -- add
                 ,width_           = maybestringopt "width" rawopts    -- register
                 ,reportopts_ = defreportopts {
                                 begin_     = maybesmartdateopt d "begin" rawopts
                                ,end_       = maybesmartdateopt d "end" rawopts
                                ,period_    = maybeperiodopt d rawopts
                                ,cleared_   = boolopt "cleared" rawopts
                                ,uncleared_ = boolopt "uncleared" rawopts
                                ,cost_      = boolopt "cost" rawopts
                                ,depth_     = maybeintopt "depth" rawopts
                                ,display_   = maybedisplayopt d rawopts
                                ,date2_     = boolopt "date2" rawopts
                                ,empty_     = boolopt "empty" rawopts
                                ,no_elide_  = boolopt "no-elide" rawopts
                                ,real_      = boolopt "real" rawopts
                                ,balancetype_ = balancetypeopt rawopts -- balance
                                ,flat_      = boolopt "flat" rawopts -- balance
                                ,drop_      = intopt "drop" rawopts -- balance
                                ,no_total_  = boolopt "no-total" rawopts -- balance
                                ,daily_     = boolopt "daily" rawopts
                                ,weekly_    = boolopt "weekly" rawopts
                                ,monthly_   = boolopt "monthly" rawopts
                                ,quarterly_ = boolopt "quarterly" rawopts
                                ,yearly_    = boolopt "yearly" rawopts
                                ,format_    = maybestringopt "format" rawopts
                                ,average_   = boolopt "average" rawopts  -- register
                                ,related_   = boolopt "related" rawopts  -- register
                                ,query_     = unwords $ listofstringopt "args" rawopts
                                }
                 }

    -- | Parse hledger CLI options from these command line arguments and
    -- add-on command names, or raise any error.
    argsToCliOpts :: [String] -> [String] -> IO CliOpts
    argsToCliOpts args addons = do
      let
        args'        = moveFlagsAfterCommand args
        cmdargsopts  = System.Console.CmdArgs.Explicit.processValue (mainmode addons) args'
        cmdargsopts' = decodeRawOpts cmdargsopts
      rawOptsToCliOpts cmdargsopts' >>= checkCliOpts

    -- | A hacky workaround for cmdargs not accepting flags before the
    -- subcommand name: try to detect and move such flags after the
    -- command.  This allows the user to put them in either position.
    -- The order of options is not preserved, but this should be ok.
    --
    -- Since we're not parsing flags as precisely as cmdargs here, this is
    -- imperfect. We make a decent effort to:
    -- - move all no-argument help and input flags
    -- - move all required-argument help and input flags along with their values, space-separated or not
    -- - not confuse things further or cause misleading errors.
    moveFlagsAfterCommand :: [String] -> [String]
    moveFlagsAfterCommand args = move args
      where
        move (f:a:as)           | isMovableNoArgFlag f           = (move $ a:as) ++ [f]
        move (f:v:a:as)         | isMovableReqArgFlag f          = (move $ a:as) ++ [f,v]
        move (fv:a:as)          | isMovableReqArgFlagAndValue fv = (move $ a:as) ++ [fv]
        move ("--debug":v:a:as) | not (null v) && all isDigit v  = (move $ a:as) ++ ["--debug",v]
        move ("--debug":a:as)                                    = (move $ a:as) ++ ["--debug"]
        move (fv@('-':'-':'d':'e':'b':'u':'g':'=':_):a:as)       = (move $ a:as) ++ [fv]
        move as = as

        isMovableNoArgFlag a  = "-" `isPrefixOf` a && dropWhile (=='-') a `elem` noargflagstomove
        isMovableReqArgFlag a = "-" `isPrefixOf` a && dropWhile (=='-') a `elem` reqargflagstomove
        isMovableReqArgFlagAndValue ('-':'-':a:as) = case break (== '=') (a:as) of (f:fs,_) -> (f:fs) `elem` reqargflagstomove
                                                                                   _        -> False
        isMovableReqArgFlagAndValue ('-':f:_:_) = [f] `elem` reqargflagstomove
        isMovableReqArgFlagAndValue _ = False

        noargflagstomove  = concatMap flagNames $ filter ((==FlagNone).flagInfo) flagstomove
        reqargflagstomove = concatMap flagNames $ filter ((==FlagReq ).flagInfo) flagstomove
        flagstomove = inputflags ++ helpflags

    -- | Convert possibly encoded option values to regular unicode strings.
    decodeRawOpts = map (\(name,val) -> (name, fromSystemString val))

    -- | Do final validation of processed opts, raising an error if there is trouble.
    checkCliOpts :: CliOpts -> IO CliOpts -- or pure..
    checkCliOpts opts@CliOpts{reportopts_=ropts} = do
      case formatFromOpts ropts of
        Left err -> optserror $ "could not parse format option: "++err
        Right _ -> return ()
      case widthFromOpts opts of
        Left err -> optserror $ "could not parse width option: "++err
        Right _ -> return ()
      return opts

    --
    -- utils
    --

    -- | Get the unique suffixes (without hledger-) of hledger-* executables
    -- found in the current user's PATH, or the empty list if there is any
    -- problem.
    getHledgerAddonCommands :: IO [String]
    getHledgerAddonCommands = map (drop (length progname + 1)) `fmap` getHledgerExesInPath

    -- | Get the unique names of hledger-*{,.hs} executables found in the current
    -- user's PATH, or the empty list if there is any problem.
    getHledgerExesInPath :: IO [String]
    getHledgerExesInPath = do
      pathdirs <- splitOn ":" `fmap` getEnvSafe "PATH"
      pathfiles <- concat `fmap` mapM getDirectoryContentsSafe pathdirs
      let hledgernamed = nub $ sort $ filter isHledgerNamed pathfiles
      -- hledgerexes <- filterM isExecutable hledgernamed
      return hledgernamed

    -- isExecutable f = getPermissions f >>= (return . executable)

    isHledgerNamed = isRight . parsewith (do
            string progname
            char '-'
            many1 (letter <|> char '-')
            optional $ (string ".hs" <|> string ".lhs")
            eof
            )

    getEnvSafe v = getEnv v `C.catch` (\(_::C.IOException) -> return "")
    getDirectoryContentsSafe d = getDirectoryContents d `C.catch` (\(_::C.IOException) -> return [])

    -- | Raise an error, showing the specified message plus a hint about --help.
    optserror = error' . (++ " (run with --help for usage)")

    setopt name val = (++ [(name,singleQuoteIfNeeded val)])

    setboolopt name = (++ [(name,"")])

    -- | Is the named option present ?
    inRawOpts :: String -> RawOpts -> Bool
    inRawOpts name = isJust . lookup name

    boolopt = inRawOpts

    maybestringopt name = maybe Nothing (Just . stripquotes) . lookup name

    stringopt name = fromMaybe "" . maybestringopt name

    listofstringopt name rawopts = [v | (k,v) <- rawopts, k==name]

    maybeintopt :: String -> RawOpts -> Maybe Int
    maybeintopt name rawopts =
        let ms = maybestringopt name rawopts in
        case ms of Nothing -> Nothing
                   Just s -> Just $ readDef (optserror $ "could not parse "++name++" number: "++s) s

    intopt name = fromMaybe 0 . maybeintopt name

    maybesmartdateopt :: Day -> String -> RawOpts -> Maybe Day
    maybesmartdateopt d name rawopts =
            case maybestringopt name rawopts of
              Nothing -> Nothing
              Just s -> either
                        (\e -> optserror $ "could not parse "++name++" date: "++show e)
                        Just
                        $ fixSmartDateStrEither' d s

    maybedisplayopt :: Day -> RawOpts -> Maybe DisplayExp
    maybedisplayopt d rawopts =
        maybe Nothing (Just . regexReplaceBy "\\[.+?\\]" fixbracketeddatestr) $ maybestringopt "display" rawopts
        where
          fixbracketeddatestr "" = ""
          fixbracketeddatestr s = "[" ++ fixSmartDateStr d (init $ tail s) ++ "]"

    maybeperiodopt :: Day -> RawOpts -> Maybe (Interval,DateSpan)
    maybeperiodopt d rawopts =
        case maybestringopt "period" rawopts of
          Nothing -> Nothing
          Just s -> either
                    (\e -> optserror $ "could not parse period option: "++show e)
                    Just
                    $ parsePeriodExpr d s

    balancetypeopt :: RawOpts -> BalanceType
    balancetypeopt rawopts
        | length [o | o <- ["cumulative","historical"], isset o] > 1
                             = optserror "please specify at most one of --cumulative and --historical"
        | isset "cumulative" = CumulativeBalance
        | isset "historical" = HistoricalBalance
        | otherwise          = PeriodBalance
        where
          isset = flip boolopt rawopts

    -- | Parse the format option if provided, possibly returning an error,
    -- otherwise get the default value.
    formatFromOpts :: ReportOpts -> Either String [FormatString]
    formatFromOpts = maybe (Right defaultBalanceFormatString) parseFormatString . format_

    -- | Default line format for balance report: "%20(total)  %2(depth_spacer)%-(account)"
    defaultBalanceFormatString :: [FormatString]
    defaultBalanceFormatString = [
          FormatField False (Just 20) Nothing TotalField
        , FormatLiteral "  "
        , FormatField True (Just 2) Nothing DepthSpacerField
        , FormatField True Nothing Nothing AccountField
        ]

    -- | Output width configuration (for register).
    data OutputWidth =
        TotalWidth Width    -- ^ specify the overall width
      | FieldWidths [Width] -- ^ specify each field's width
      deriving Show

    -- | A width value.
    data Width =
        Width Int -- ^ set width to exactly this number of characters
      | Auto      -- ^ set width automatically from available space
      deriving Show

    -- | Default width of hledger console output.
    defaultWidth         = 80

    -- | Width of hledger console output when the -w flag is used with no value.
    defaultWidthWithFlag = 120

    -- | Parse the width option if provided, possibly returning an error,
    -- otherwise get the default value.
    widthFromOpts :: CliOpts -> Either String OutputWidth
    widthFromOpts CliOpts{width_=Nothing} = Right $ TotalWidth $ Width defaultWidth
    widthFromOpts CliOpts{width_=Just ""} = Right $ TotalWidth $ Width defaultWidthWithFlag
    widthFromOpts CliOpts{width_=Just s}  = parseWidth s

    parseWidth :: String -> Either String OutputWidth
    parseWidth s = case (runParser outputwidthp () "(unknown)") s of
        Left  e -> Left $ show e
        Right x -> Right x

    outputwidthp :: GenParser Char st OutputWidth
    outputwidthp =
      try (do w <- widthp
              ws <- many1 (char ',' >> widthp)
              return $ FieldWidths $ w:ws)
      <|> TotalWidth `fmap` widthp

    widthp :: GenParser Char st Width
    widthp = (string "auto" >> return Auto)
        <|> (Width . read) `fmap` many1 digit

    -- | Get the account name aliases from options, if any.
    aliasesFromOpts :: CliOpts -> [(AccountName,AccountName)]
    aliasesFromOpts = map parseAlias . alias_
        where
          -- similar to ledgerAlias
          parseAlias :: String -> (AccountName,AccountName)
          parseAlias s = (accountNameWithoutPostingType $ strip orig
                         ,accountNameWithoutPostingType $ strip alias')
              where
                (orig, alias) = break (=='=') s
                alias' = case alias of ('=':rest) -> rest
                                       _ -> orig

    -- | Get the (tilde-expanded, absolute) journal file path from
    -- 1. options, 2. an environment variable, or 3. the default.
    journalFilePathFromOpts :: CliOpts -> IO String
    journalFilePathFromOpts opts = do
      f <- defaultJournalPath
      d <- getCurrentDirectory
      expandPath d $ fromMaybe f $ file_ opts

    -- | Get the (tilde-expanded) rules file path from options, if any.
    rulesFilePathFromOpts :: CliOpts -> IO (Maybe FilePath)
    rulesFilePathFromOpts opts = do
      d <- getCurrentDirectory
      maybe (return Nothing) (fmap Just . expandPath d) $ rules_file_ opts

    -- | Get a mode's help message as a nicely wrapped string.
    showModeHelp :: Mode a -> String
    showModeHelp =
      (showText defaultWrap :: [Text] -> String)
      .
      (helpText [] HelpFormatDefault :: Mode a -> [Text])

    -- not used:

    -- | Print debug info about arguments and options if --debug is present.
    debugArgs :: [String] -> CliOpts -> IO ()
    debugArgs args opts =
      when ("--debug" `elem` args) $ do
        progname <- getProgName
        putStrLn $ "running: " ++ progname
        putStrLn $ "raw args: " ++ show args
        putStrLn $ "processed opts:\n" ++ show opts
        d <- getCurrentDay
        putStrLn $ "search query: " ++ (show $ queryFromOpts d $ reportopts_ opts)

    -- not used:

    -- | Parse hledger CLI options from the command line using the given
    -- cmdargs mode, and either return them or, if a help flag is present,
    -- print the mode help and exit the program.
    getCliOpts :: Mode RawOpts -> IO CliOpts
    getCliOpts mode = do
      args <- getArgs
      let rawopts = decodeRawOpts $ processValue mode args
      opts <- rawOptsToCliOpts rawopts >>= checkCliOpts
      debugArgs args opts
      -- if any (`elem` args) ["--help","-h","-?"]
      when ("help" `inRawOpts` rawopts_ opts) $
        putStr (showModeHelp mode) >> exitSuccess
      return opts

    tests_Hledger_Cli_Options = TestList
     [
     ]