{-
Bustle.Stats: calculates statistics for D-Bus logs
Copyright © 2009–2011 Collabora Ltd.

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
-}
module Bustle.Stats
  ( TallyType(..)
  , FrequencyInfo(..)
  , frequencies

  , methodTimes
  , TimeInfo(..)

  , messageSizes
  , SizeType(..)
  , SizeInfo(..)
  )
where

import Control.Monad (guard)
import Data.List (sort, sortBy)
import Data.Maybe (mapMaybe)
import Data.Ord (comparing)

import qualified Data.Map as Map
import Data.Map (Map)

import Bustle.Types

data TallyType = TallyMethod | TallySignal
    deriving (Eq, Ord, Show)

repr :: DetailedEvent
     -> Maybe (TallyType, Maybe InterfaceName, MemberName)
repr (Detailed _ (NOCEvent _) _) = Nothing
repr (Detailed _ (MessageEvent msg) _) =
    case msg of
        MethodCall { member = m } -> Just (TallyMethod, iface m, membername m)
        Signal     { member = m } -> Just (TallySignal, iface m, membername m)
        _                         -> Nothing

data FrequencyInfo =
    FrequencyInfo { fiFrequency :: Int
                  , fiType :: TallyType
                  , fiInterface :: Maybe InterfaceName
                  , fiMember :: MemberName
                  }
  deriving (Show, Eq, Ord)

frequencies :: Log -> [FrequencyInfo]
frequencies = reverse
            . sort
            . map (\((t, i, m), c) -> FrequencyInfo c t i m)
            . Map.toList
            . foldr (Map.alter alt) Map.empty
            . mapMaybe repr
    where alt Nothing  = Just 1
          alt (Just n) = Just (n + 1)

mean :: (Eq a, Fractional a) => [a] -> a
mean = acc 0 0
   where acc 0 _ [] = error "mean of empty list"
         acc n t [] = t / n
         acc n t (x:xs) = acc (n + 1) (t + x) xs

data TimeInfo =
    TimeInfo { tiInterface :: Maybe InterfaceName
             , tiMethodName :: MemberName
             , tiTotalTime :: Double -- milliseconds
             , tiCallFrequency :: Int
             , tiMeanCallTime :: Double -- milliseconds
             }

methodTimes :: Log
            -> [TimeInfo]
methodTimes = reverse
            . sortBy (comparing tiTotalTime)
            . map summarize
            . Map.toList
            . foldr (\(i, method, time) ->
                        Map.alter (alt time) (i, method)) Map.empty
            . mapMaybe methodReturn
            -- Get rid of NOC messages
            . snd
            . partitionDetaileds
    where alt newtime Nothing = Just (newtime, [newtime])
          alt newtime (Just (total, times)) =
              Just (newtime + total, newtime : times)

          isReturn :: Message -> Bool
          isReturn (MethodReturn {}) = True
          isReturn _                 = False

          methodReturn :: Detailed Message
                       -> Maybe (Maybe InterfaceName, MemberName, Microseconds)
          methodReturn dm = do
              let m = deEvent dm
              guard (isReturn m)
              Detailed start (call@(MethodCall {})) _ <- inReplyTo m
              return ( iface (member call)
                     , membername (member call)
                     , deTimestamp dm - start
                     )

          summarize ((i, method), (total, times)) =
              TimeInfo { tiInterface = i
                       , tiMethodName = method
                       , tiTotalTime = fromIntegral total / 1000
                       , tiCallFrequency = length times
                       , tiMeanCallTime = (mean $ map fromIntegral times) / 1000
                       }

-- FIXME: really? again?
data SizeType = SizeCall
              | SizeReturn
              | SizeError
              | SizeSignal
  deriving
    (Show, Ord, Eq)

-- The fields are in this ideosyncratic order to make the derived Ord instance
-- do what we want
data SizeInfo =
    SizeInfo { siMeanSize, siMaxSize, siMinSize :: Int
             , siType :: SizeType
             , siInterface :: Maybe InterfaceName
             , siName :: MemberName
             }
  deriving
    (Show, Ord, Eq)

messageSizes :: Log
             -> [SizeInfo]
messageSizes messages =
    reverse . sort . map summarize $ Map.assocs sizeTable
  where
    summarize :: ((SizeType, Maybe InterfaceName, MemberName), [Int]) -> SizeInfo
    summarize ((t, i, m), sizes) =
        SizeInfo (intMean sizes) (maximum sizes) (minimum sizes) t i m

    intMean :: [Int] -> Int
    intMean = ceiling . (mean :: [Double] -> Double) . map fromIntegral

    sizeTable = foldr f Map.empty . snd . partitionDetaileds $ messages

    f :: Detailed Message
      -> Map (SizeType, Maybe InterfaceName, MemberName) [Int]
      -> Map (SizeType, Maybe InterfaceName, MemberName) [Int]
    f dm = case (sizeKeyRepr dm, deDetails dm) of
        (Just key, Just (size, _)) -> Map.insertWith' (++) key [size]
        _                          -> id

    callDetails :: Message
                -> Maybe (Maybe InterfaceName, MemberName)
    callDetails msg = do
        Detailed _ msg' _ <- inReplyTo msg
        return (iface (member msg'), membername (member msg'))

    sizeKeyRepr :: Detailed Message
                -> Maybe (SizeType, Maybe InterfaceName, MemberName)
    sizeKeyRepr dm = do
        let msg = deEvent dm
        case msg of
            MethodCall { member = m } -> return (SizeCall, iface m, membername m)
            Signal     { member = m } -> return (SizeSignal, iface m, membername m)
            MethodReturn { } -> do
                (x, y) <- callDetails msg
                return (SizeReturn, x, y)
            Error { } -> do
                (x, y) <- callDetails msg
                return (SizeError, x, y)
