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

-- |
-- Module      : Gauge.Main.Options
-- Copyright   : (c) 2014 Bryan O'Sullivan
--
-- License     : BSD-style
-- Maintainer  : bos@serpentine.com
-- Stability   : experimental
-- Portability : GHC
--
-- Benchmarking command-line configuration.

module Gauge.Main.Options
    ( defaultConfig
    , makeSelector
    , parseWith
    , describe
    , versionInfo
    , Config (..)
    , Verbosity (..)
    , DisplayMode (..)
    , MatchType (..)
    , Mode (..)
    ) where

-- Temporary: to support pre-AMP GHC 7.8.4:
import Data.Monoid

import Gauge.Measurement (validateAccessors)
import Gauge.Time (MilliSeconds(..))
import Data.Char (isSpace, toLower)
import Data.List (foldl')
import Data.Version (showVersion)
import System.Console.GetOpt
import Paths_gauge (version)
import Data.Data (Data, Typeable)
import Data.Int (Int64)
import Data.List (isInfixOf, isPrefixOf)
import GHC.Generics (Generic)

-- | Control the amount of information displayed.
data Verbosity = Quiet
               | Normal
               | Verbose
                 deriving (Eq, Ord, Bounded, Enum, Read, Show, Typeable, Data,
                           Generic)

-- | How to match a benchmark name.
data MatchType = Prefix
                 -- ^ Match by prefix. For example, a prefix of
                 -- @\"foo\"@ will match @\"foobar\"@.
               | Pattern
                 -- ^ Match by searching given substring in benchmark
                 -- paths.
               | IPattern
                 -- ^ Same as 'Pattern', but case insensitive.
               deriving (Eq, Ord, Bounded, Enum, Read, Show, Typeable, Data,
                         Generic)

-- | Execution mode for a benchmark program.
data Mode = List
            -- ^ List all benchmarks.
          | Version
            -- ^ Print the version.
          | Help
            -- ^ Print help
          | DefaultMode
            -- ^ Default Benchmark mode
          deriving (Eq, Read, Show, Typeable, Data, Generic)

data DisplayMode =
      Condensed
    | StatsTable
    deriving (Eq, Read, Show, Typeable, Data, Generic)

-- | Top-level benchmarking configuration.
data Config = Config {
      confInterval :: Maybe Double
      -- ^ Confidence interval for bootstrap estimation (greater than
      -- 0, less than 1).
    , forceGC      :: Bool
      -- ^ /Obsolete, unused/.  This option used to force garbage
      -- collection between every benchmark run, but it no longer has
      -- an effect (we now unconditionally force garbage collection).
      -- This option remains solely for backwards API compatibility.
    , timeLimit    :: Maybe Double
      -- ^ Number of seconds to run a single benchmark.  In practice, execution
      -- time may exceed this limit to honor minimum number of samples or
      -- minimum duration of each sample. Increased time limit allows us to
      -- take more samples. Use 0 for a single sample benchmark.
    , minSamples   :: Maybe Int
      -- ^ Minimum number of samples to be taken.
    , minDuration  :: MilliSeconds
      -- ^ Minimum duration of each sample, increased duration allows us to
      -- perform more iterations in each sample. To enforce a single iteration
      -- in a sample use duration 0.
    , includeFirstIter :: Bool
      -- ^ Discard the very first iteration of a benchmark. The first iteration
      -- includes the potentially extra cost of one time evaluations
      -- introducing large variance.
    , quickMode    :: Bool
    -- ^ Quickly measure and report raw measurements.
    , measureOnly  :: Maybe FilePath
    -- ^ Just measure the given benchmark and place the raw output in this
    -- file, do not analyse and generate a report.
    , measureWith  :: Maybe FilePath
    -- ^ Specify the path of the benchmarking program to use (this program
    -- itself) for measuring the benchmarks in a separate process.
    , resamples    :: Int
      -- ^ Number of resamples to perform when bootstrapping.
    , regressions  :: [([String], String)]
      -- ^ Regressions to perform.
    , rawDataFile  :: Maybe FilePath
      -- ^ File to write binary measurement and analysis data to.  If
      -- not specified, this will be a temporary file.
    , reportFile   :: Maybe FilePath
      -- ^ File to write report output to, with template expanded.
    , csvFile      :: Maybe FilePath
      -- ^ File to write CSV summary to.
    , csvRawFile   :: Maybe FilePath
      -- ^ File to write CSV measurements to.
    , jsonFile     :: Maybe FilePath
      -- ^ File to write JSON-formatted results to.
    , junitFile    :: Maybe FilePath
      -- ^ File to write JUnit-compatible XML results to.
    , verbosity    :: Verbosity
      -- ^ Verbosity level to use when running and analysing
      -- benchmarks.
    , template     :: FilePath
      -- ^ Template file to use if writing a report.
    , iters        :: Maybe Int64
      -- ^ Number of iterations
    , match        :: MatchType
      -- ^ Type of matching to use, if any
    , mode         :: Mode
      -- ^ Mode of operation
    , displayMode  :: DisplayMode
    } deriving (Eq, Read, Show, Typeable, Data, Generic)

-- | Default benchmarking configuration.
defaultConfig :: Config
defaultConfig = Config
    { confInterval     = Nothing
    , forceGC          = True
    , timeLimit        = Nothing
    , minSamples       = Nothing
    , minDuration      = MilliSeconds 30
    , includeFirstIter = False
    , quickMode        = False
    , measureOnly      = Nothing
    , measureWith      = Nothing
    , resamples        = 1000
    , regressions      = []
    , rawDataFile      = Nothing
    , reportFile       = Nothing
    , csvFile          = Nothing
    , csvRawFile       = Nothing
    , jsonFile         = Nothing
    , junitFile        = Nothing
    , verbosity        = Normal
    , template         = "default"
    , iters            = Nothing
    , match            = Prefix
    , mode             = DefaultMode
    , displayMode      = StatsTable
    }

-- | Create a benchmark selector function that can tell if a name given on the
-- command line matches a defined benchmark.
makeSelector :: MatchType
            -> [String]
            -- ^ Command line arguments.
            -> (String -> Bool)
makeSelector matchKind args =
  case matchKind of
    Prefix   -> \b -> null args || any (`isPrefixOf` b) args
    Pattern  -> \b -> null args || any (`isInfixOf` b) args
    IPattern -> \b -> null args || any (`isInfixOf` map toLower b) (map (map toLower) args)

parseWith :: Config
            -- ^ Default configuration to use
          -> [String]
            -- ^ Program Argument
          -> (Config, [String])
parseWith start argv =
    case getOpt Permute opts argv of
        (o,n,[]  ) -> (foldl' (flip id) start o, n)
        (_,_,errs) -> optionError (concat errs ++ usageInfo header opts)

opts :: [OptDescr (Config -> Config)]
opts =
    [ Option "I" ["ci"]         (ReqArg setCI "CI") "Confidence interval"
    , Option "G" ["no-gc"]      (NoArg setNoGC)     "Do not collect garbage between iterations"
    , Option "L" ["time-limit"] (ReqArg setTimeLimit "SECS") "Time limit to run a benchmark, use 0 to force min-samples per benchmark"
    , Option ""  ["min-samples"] (ReqArg setMinSamples "COUNT") "Minimum number of samples to be taken"
    , Option ""  ["min-duration"] (ReqArg setMinDuration "MILLISECS") "Minimum duration of each sample, use 0 to force one iteration per sample"
    , Option ""  ["include-first-iter"] (NoArg setIncludeFirst) "Do not discard the measurement of the first iteration"
    , Option "q" ["quick"]      (NoArg setQuickMode) "Perform a quick measurement and report results without statistical analysis"
    , Option ""  ["measure-only"] (fileArg setMeasureOnly) "Just measure the benchmark and place the raw data in the given file"
    , Option ""  ["measure-with"] (fileArg setMeasureProg) "Perform measurements in a separate process using this program."
    , Option ""  ["resamples"]  (ReqArg setResamples "COUNT") "Number of boostrap resamples to perform"
    , Option ""  ["regress"]    (ReqArg setRegressions "RESP:PRED..") "Regressions to perform"
    , Option ""  ["raw"]        (fileArg setRaw) "File to write raw data to"
    , Option "o" ["output"]     (fileArg setOutput) "File to write report to"
    , Option ""  ["csvraw"]     (fileArg setCSVRaw) "File to write CSV measurements to"
    , Option ""  ["csv"]        (fileArg setCSV) "File to write CSV summary to"
    , Option ""  ["json"]       (fileArg setJSON) "File to write JSON summary to"
    , Option ""  ["junit"]      (fileArg setJUnit) "File to write JUnit summary to"
    , Option "v" ["verbosity"]  (ReqArg setVerbosity "LEVEL") "Verbosity level"
    , Option "t" ["template"]   (fileArg setTemplate) "Template to use for report"
    , Option "n" ["iters"]      (ReqArg setIters "ITERS") "Run benchmarks, don't analyse"
    , Option "m" ["match"]      (ReqArg setMatch "MATCH") "How to match benchmark names: prefix, glob, pattern, or ipattern"
    , Option "l" ["list"]       (NoArg $ setMode List) "List benchmarks"
    , Option ""  ["version"]    (NoArg $ setMode Version) "Show version info"
    , Option "s" ["small"]      (NoArg $ setDisplayMode Condensed) "Set benchmark display to the minimum useful information"
    , Option "h" ["help"]       (NoArg $ setMode Help) "Show help"
    ]
  where
    fileArg f = ReqArg f "FILE"
    setCI s v = v { confInterval = Just $ range 0.001 0.999 s }
    setNoGC v = v { forceGC = False }
    setTimeLimit s v = v { timeLimit = Just $ range 0.0 86400 s }
    setMinSamples n v = v { minSamples = Just $ read n }
    setMinDuration ms v = v { minDuration = MilliSeconds $ read ms }
    setIncludeFirst v = v { includeFirstIter = True }
    setQuickMode v = v { quickMode = True }
    setMeasureOnly f v = v { measureOnly = Just f }
    setMeasureProg f v = v { measureWith = Just f }
    setResamples s v = v { resamples = range 1 1000000 s }
    setRegressions s v = v { regressions = regressParams s : regressions v }
    setRaw f v = v { rawDataFile = Just f }
    setOutput f v = v { reportFile = Just f }
    setCSV f v = v { csvFile = Just f }
    setCSVRaw f v = v { csvRawFile = Just f }
    setJSON f v = v { jsonFile = Just f }
    setJUnit f v = v { junitFile = Just f }
    setVerbosity s v = v { verbosity = toEnum (range 0 2 s) }
    setTemplate f v = v { template = f }
    setIters s v = v { iters = Just $ read s }
    setMatch s v =
        let m = case map toLower s of
                    "pfx"      -> Prefix
                    "prefix"   -> Prefix
                    "pattern"  -> Pattern
                    "ipattern" -> IPattern
                    _          -> optionError ("unknown match type: " <> s)
         in v { match = m }
    setMode m v = v { mode = m }
    setDisplayMode m v = v { displayMode = m }

-- FIXME
optionError :: String -> a
optionError s = error s

range :: (Show a, Read a, Ord a) => a -> a -> String -> a
range lo hi s = do
    case reads s of
        [(i, "")]
            | i >= lo && i <= hi -> i
            | otherwise          -> optionError $ show i ++ " is outside range " ++ show (lo,hi)
        _ -> optionError $ show s ++ " is not a number"

{-
Regression metrics (for use with --regress):
  time                     wall-clock time
  cpuTime                  CPU time
  cycles                   CPU cycles
  iters                    loop iterations
  allocated                (+RTS -T) bytes allocated
  numGcs                   (+RTS -T) number of garbage collections
  bytesCopied              (+RTS -T) number of bytes copied during GC
  mutatorWallSeconds       (+RTS -T) wall-clock time for mutator threads
  mutatorCpuSeconds        (+RTS -T) CPU time spent running mutator threads
  gcWallSeconds            (+RTS -T) wall-clock time spent doing GC
  gcCpuSeconds             (+RTS -T) CPU time spent doing GC
Benchmark self: FINISH

-- We sort not by name, but by likely frequency of use.
regressionHelp :: Chunk Doc
regressionHelp =
    fmap (text "Regression metrics (for use with --regress):" .$.) $
      tabulate [(text n,text d) | (n,(_,d)) <- map f measureKeys]
  where f k = (k, measureAccessors M.! k)
  -}

describe :: String
describe = usageInfo header opts

header :: String
header = "Microbenchmark suite - " <> versionInfo

-- | A string describing the version of this benchmark (really, the
-- version of gauge that was used to build it).
versionInfo :: String
versionInfo = "built with gauge " <> showVersion version

regressParams :: String -> ([String], String)
regressParams m
    | null r    = optionError "no responder specified"
    | null ps   = optionError "no predictors specified"
    | otherwise =
        let ret = (words . map repl . drop 1 $ ps, tidy r)
        in either optionError (const ret) $ uncurry validateAccessors ret
  where
      repl ','   = ' '
      repl c     = c
      tidy       = reverse . dropWhile isSpace . reverse . dropWhile isSpace
      (r,ps)     = break (==':') m