    {-# LANGUAGE RecordWildCards, DeriveDataTypeable #-}
    {-|

    Options common to most hledger reports.

    -}

    module Hledger.Reports.ReportOptions (
      ReportOpts(..),
      BalanceType(..),
      AccountListMode(..),
      FormatStr,
      defreportopts,
      rawOptsToReportOpts,
      flat_,
      tree_,
      dateSpanFromOpts,
      intervalFromOpts,
      clearedValueFromOpts,
      whichDateFromOpts,
      journalSelectingAmountFromOpts,
      queryFromOpts,
      queryFromOptsOnly,
      queryOptsFromOpts,
      transactionDateFn,
      postingDateFn,

      tests_Hledger_Reports_ReportOptions
    )
    where

    import Data.Data (Data)
    import Data.Typeable (Typeable)
    import Data.Time.Calendar
    import System.Console.CmdArgs.Default  -- some additional default stuff
    import Test.HUnit

    import Hledger.Data
    import Hledger.Query
    import Hledger.Utils

    type FormatStr = String

    -- | Which balance is being shown in a multi-column balance report.
    data BalanceType = PeriodBalance     -- ^ The change of balance in each period.
                     | CumulativeBalance -- ^ The accumulated balance at each period's end, starting from zero at the report start date.
                     | HistoricalBalance -- ^ The historical balance at each period's end, starting from the account balances at the report start date.
      deriving (Eq,Show,Data,Typeable)

    instance Default BalanceType where def = PeriodBalance

    -- | Should accounts be displayed: in the command's default style, hierarchically, or as a flat list ?
    data AccountListMode = ALDefault | ALTree | ALFlat deriving (Eq, Show, Data, Typeable)

    instance Default AccountListMode where def = ALDefault

    -- | Standard options for customising report filtering and output,
    -- corresponding to hledger's command-line options and query language
    -- arguments. Used in hledger-lib and above.
    data ReportOpts = ReportOpts {
         begin_          :: Maybe Day
        ,end_            :: Maybe Day
        ,period_         :: Maybe (Interval,DateSpan)
        ,cleared_        :: Bool
        ,pending_        :: Bool
        ,uncleared_      :: Bool
        ,cost_           :: Bool
        ,depth_          :: Maybe Int
        ,display_        :: Maybe DisplayExp
        ,date2_          :: Bool
        ,empty_          :: Bool
        ,no_elide_       :: Bool
        ,real_           :: Bool
        ,daily_          :: Bool
        ,weekly_         :: Bool
        ,monthly_        :: Bool
        ,quarterly_      :: Bool
        ,yearly_         :: Bool
        ,format_         :: Maybe FormatStr
        ,query_          :: String -- all arguments, as a string
        -- register
        ,average_        :: Bool
        ,related_        :: Bool
        -- balance
        ,balancetype_    :: BalanceType
        ,accountlistmode_  :: AccountListMode
        ,drop_           :: Int
        ,row_total_      :: Bool
        ,no_total_       :: Bool
     } deriving (Show, Data, Typeable)

    instance Default ReportOpts where def = defreportopts

    defreportopts :: ReportOpts
    defreportopts = ReportOpts
        def
        def
        def
        def
        def
        def
        def
        def
        def
        def
        def
        def
        def
        def
        def
        def
        def
        def
        def
        def
        def
        def
        def
        def
        def
        def
        def

    rawOptsToReportOpts :: RawOpts -> IO ReportOpts
    rawOptsToReportOpts rawopts = do
      d <- getCurrentDay
      return defreportopts{
         begin_       = maybesmartdateopt d "begin" rawopts
        ,end_         = maybesmartdateopt d "end" rawopts
        ,period_      = maybeperiodopt d rawopts
        ,cleared_     = boolopt "cleared" rawopts
        ,pending_     = boolopt "pending" 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
        ,daily_       = boolopt "daily" rawopts
        ,weekly_      = boolopt "weekly" rawopts
        ,monthly_     = boolopt "monthly" rawopts
        ,quarterly_   = boolopt "quarterly" rawopts
        ,yearly_      = boolopt "yearly" rawopts
        ,format_      = maybestringopt "format" rawopts
        ,query_       = unwords $ listofstringopt "args" rawopts -- doesn't handle an arg like "" right
        ,average_     = boolopt "average" rawopts
        ,related_     = boolopt "related" rawopts
        ,balancetype_ = balancetypeopt rawopts
        ,accountlistmode_ = accountlistmodeopt rawopts
        ,drop_        = intopt "drop" rawopts
        ,row_total_   = boolopt "row-total" rawopts
        ,no_total_    = boolopt "no-total" rawopts
        }

    accountlistmodeopt :: RawOpts -> AccountListMode
    accountlistmodeopt rawopts =
      case reverse $ filter (`elem` ["tree","flat"]) $ map fst rawopts of
        ("tree":_) -> ALTree
        ("flat":_) -> ALFlat
        _          -> ALDefault

    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

    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

    type DisplayExp = String

    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

    -- | Legacy-compatible convenience aliases for accountlistmode_.
    tree_ :: ReportOpts -> Bool
    tree_ = (==ALTree) . accountlistmode_

    flat_ :: ReportOpts -> Bool
    flat_ = (==ALFlat) . accountlistmode_

    -- | Figure out the date span we should report on, based on any
    -- begin/end/period options provided. A period option will cause begin and
    -- end options to be ignored.
    dateSpanFromOpts :: Day -> ReportOpts -> DateSpan
    dateSpanFromOpts _ ReportOpts{..} =
        case period_ of Just (_,span) -> span
                        Nothing -> DateSpan begin_ end_

    -- | Figure out the reporting interval, if any, specified by the options.
    -- --period overrides --daily overrides --weekly overrides --monthly etc.
    intervalFromOpts :: ReportOpts -> Interval
    intervalFromOpts ReportOpts{..} =
        case period_ of
          Just (interval,_) -> interval
          Nothing -> i
              where i | daily_ = Days 1
                      | weekly_ = Weeks 1
                      | monthly_ = Months 1
                      | quarterly_ = Quarters 1
                      | yearly_ = Years 1
                      | otherwise =  NoInterval

    -- | Get a maybe boolean representing the last cleared/uncleared option if any.
    clearedValueFromOpts :: ReportOpts -> Maybe ClearedStatus
    clearedValueFromOpts ReportOpts{..} | cleared_   = Just Cleared
                                        | pending_   = Just Pending
                                        | uncleared_ = Just Uncleared
                                        | otherwise  = Nothing

    -- depthFromOpts :: ReportOpts -> Int
    -- depthFromOpts opts = min (fromMaybe 99999 $ depth_ opts) (queryDepth $ queryFromOpts nulldate opts)

    -- | Report which date we will report on based on --date2.
    whichDateFromOpts :: ReportOpts -> WhichDate
    whichDateFromOpts ReportOpts{..} = if date2_ then SecondaryDate else PrimaryDate

    -- | Select the Transaction date accessor based on --date2.
    transactionDateFn :: ReportOpts -> (Transaction -> Day)
    transactionDateFn ReportOpts{..} = if date2_ then transactionDate2 else tdate

    -- | Select the Posting date accessor based on --date2.
    postingDateFn :: ReportOpts -> (Posting -> Day)
    postingDateFn ReportOpts{..} = if date2_ then postingDate2 else postingDate

    -- | Convert this journal's postings' amounts to the cost basis amounts if
    -- specified by options.
    journalSelectingAmountFromOpts :: ReportOpts -> Journal -> Journal
    journalSelectingAmountFromOpts opts
        | cost_ opts = journalConvertAmountsToCost
        | otherwise = id

    -- | Convert report options and arguments to a query.
    queryFromOpts :: Day -> ReportOpts -> Query
    queryFromOpts d opts@ReportOpts{..} = simplifyQuery $ And $ [flagsq, argsq]
      where
        flagsq = And $
                  [(if date2_ then Date2 else Date) $ dateSpanFromOpts d opts]
                  ++ (if real_ then [Real True] else [])
                  ++ (if empty_ then [Empty True] else []) -- ?
                  ++ (maybe [] ((:[]) . Status) (clearedValueFromOpts opts))
                  ++ (maybe [] ((:[]) . Depth) depth_)
        argsq = fst $ parseQuery d query_

    -- | Convert report options to a query, ignoring any non-flag command line arguments.
    queryFromOptsOnly :: Day -> ReportOpts -> Query
    queryFromOptsOnly d opts@ReportOpts{..} = simplifyQuery flagsq
      where
        flagsq = And $
                  [(if date2_ then Date2 else Date) $ dateSpanFromOpts d opts]
                  ++ (if real_ then [Real True] else [])
                  ++ (if empty_ then [Empty True] else []) -- ?
                  ++ (maybe [] ((:[]) . Status) (clearedValueFromOpts opts))
                  ++ (maybe [] ((:[]) . Depth) depth_)

    tests_queryFromOpts :: [Test]
    tests_queryFromOpts = [
     "queryFromOpts" ~: do
      assertEqual "" Any (queryFromOpts nulldate defreportopts)
      assertEqual "" (Acct "a") (queryFromOpts nulldate defreportopts{query_="a"})
      assertEqual "" (Desc "a a") (queryFromOpts nulldate defreportopts{query_="desc:'a a'"})
      assertEqual "" (Date $ mkdatespan "2012/01/01" "2013/01/01")
                     (queryFromOpts nulldate defreportopts{begin_=Just (parsedate "2012/01/01")
                                                          ,query_="date:'to 2013'"
                                                          })
      assertEqual "" (Date2 $ mkdatespan "2012/01/01" "2013/01/01")
                     (queryFromOpts nulldate defreportopts{query_="date2:'in 2012'"})
      assertEqual "" (Or [Acct "a a", Acct "'b"])
                     (queryFromOpts nulldate defreportopts{query_="'a a' 'b"})
     ]

    -- | Convert report options and arguments to query options.
    queryOptsFromOpts :: Day -> ReportOpts -> [QueryOpt]
    queryOptsFromOpts d ReportOpts{..} = flagsqopts ++ argsqopts
      where
        flagsqopts = []
        argsqopts = snd $ parseQuery d query_

    tests_queryOptsFromOpts :: [Test]
    tests_queryOptsFromOpts = [
     "queryOptsFromOpts" ~: do
      assertEqual "" [] (queryOptsFromOpts nulldate defreportopts)
      assertEqual "" [] (queryOptsFromOpts nulldate defreportopts{query_="a"})
      assertEqual "" [] (queryOptsFromOpts nulldate defreportopts{begin_=Just (parsedate "2012/01/01")
                                                                 ,query_="date:'to 2013'"
                                                                 })
     ]

    tests_Hledger_Reports_ReportOptions :: Test
    tests_Hledger_Reports_ReportOptions = TestList $
        tests_queryFromOpts
     ++ tests_queryOptsFromOpts