    {-# LANGUAGE OverloadedStrings #-}

    -- | Encoding of ekg metrics as JSON. The encoding defined by the
    -- functions in this module are standardized and used by the ekg web
    -- UI. The purpose of this module is to let other web servers and
    -- frameworks than the one used by the ekg package expose ekg metrics.
    module System.Metrics.Json
        ( -- * Converting metrics to JSON values
          sampleToJson
        , valueToJson

          -- ** Newtype wrappers with instances
        , Sample(..)
        , Value(..)
        ) where

    import Data.Aeson ((.=))
    import qualified Data.Aeson.Types as A
    import qualified Data.HashMap.Strict as M
    import Data.Int (Int64)
    import qualified Data.Text as T
    import qualified System.Metrics as Metrics
    import qualified System.Metrics.Distribution as Distribution

    ------------------------------------------------------------------------
    -- * Converting metrics to JSON values

    -- | Encode metrics as nested JSON objects. Each "." in the metric
    -- name introduces a new level of nesting. For example, the metrics
    -- @[("foo.bar", 10), ("foo.baz", "label")]@ are encoded as
    --
    -- > {
    -- >   "foo": {
    -- >     "bar": {
    -- >       "type:", "c",
    -- >       "val": 10
    -- >     },
    -- >     "baz": {
    -- >       "type": "l",
    -- >       "val": "label"
    -- >     }
    -- >   }
    -- > }
    --
    sampleToJson :: Metrics.Sample -> A.Value
    sampleToJson metrics =
        buildOne metrics $ A.emptyObject
      where
        buildOne :: M.HashMap T.Text Metrics.Value -> A.Value -> A.Value
        buildOne m o = M.foldlWithKey' build o m

        build :: A.Value -> T.Text -> Metrics.Value -> A.Value
        build m name val = go m (T.splitOn "." name) val

        go :: A.Value -> [T.Text] -> Metrics.Value -> A.Value
        go (A.Object m) [str] val      = A.Object $ M.insert str metric m
          where metric = valueToJson val
        go (A.Object m) (str:rest) val = case M.lookup str m of
            Nothing -> A.Object $ M.insert str (go A.emptyObject rest val) m
            Just m' -> A.Object $ M.insert str (go m' rest val) m
        go v _ _                        = typeMismatch "Object" v

    typeMismatch :: String   -- ^ The expected type
                 -> A.Value  -- ^ The actual value encountered
                 -> a
    typeMismatch expected actual =
        error $ "when expecting a " ++ expected ++ ", encountered " ++ name ++
        " instead"
      where
        name = case actual of
            A.Object _ -> "Object"
            A.Array _  -> "Array"
            A.String _ -> "String"
            A.Number _ -> "Number"
            A.Bool _   -> "Boolean"
            A.Null     -> "Null"

    -- | Encodes a single metric as a JSON object. Example:
    --
    -- > {
    -- >   "type": "c",
    -- >   "val": 89460
    -- > }
    --
    valueToJson :: Metrics.Value -> A.Value
    valueToJson (Metrics.Counter n)      = scalarToJson n CounterType
    valueToJson (Metrics.Gauge n)        = scalarToJson n GaugeType
    valueToJson (Metrics.Label l)        = scalarToJson l LabelType
    valueToJson (Metrics.Distribution l) = distrubtionToJson l

    -- | Convert a scalar metric (i.e. counter, gauge, or label) to a JSON
    -- value.
    scalarToJson :: A.ToJSON a => a -> MetricType -> A.Value
    scalarToJson val ty = A.object
        ["val" .= val, "type" .= metricType ty]
    {-# SPECIALIZE scalarToJson :: Int64 -> MetricType -> A.Value #-}
    {-# SPECIALIZE scalarToJson :: T.Text -> MetricType -> A.Value #-}

    data MetricType =
          CounterType
        | GaugeType
        | LabelType
        | DistributionType

    metricType :: MetricType -> T.Text
    metricType CounterType      = "c"
    metricType GaugeType        = "g"
    metricType LabelType        = "l"
    metricType DistributionType = "d"

    -- | Convert a distribution to a JSON value.
    distrubtionToJson :: Distribution.Stats -> A.Value
    distrubtionToJson stats = A.object
        [ "mean" .= Distribution.mean stats
        , "variance" .= Distribution.variance stats
        , "count" .= Distribution.count stats
        , "sum" .= Distribution.sum stats
        , "min" .= Distribution.min stats
        , "max" .= Distribution.max stats
        , "type" .= metricType DistributionType
        ]

    ------------------------------------------------------------------------
    -- ** Newtype wrappers with instances

    -- | Newtype wrapper that provides a 'A.ToJSON' instances for the
    -- underlying 'Metrics.Sample' without creating an orphan instance.
    newtype Sample = Sample Metrics.Sample
        deriving Show

    -- | Uses 'sampleToJson'.
    instance A.ToJSON Sample where
        toJSON (Sample s) = sampleToJson s

    -- | Newtype wrapper that provides a 'A.ToJSON' instances for the
    -- underlying 'Metrics.Value' without creating an orphan instance.
    newtype Value = Value Metrics.Value
        deriving Show

    -- | Uses 'valueToJson'.
    instance A.ToJSON Value where
        toJSON (Value v) = valueToJson v