{-# LANGUAGE DeriveGeneric     #-}
{-# LANGUAGE OverloadedStrings #-}
{-|
Module      : Instana.SDK.Internal.Metrics.Sample
Description : Instana's internal representation of metrics values and samples.
-}
module Instana.SDK.Internal.Metrics.Sample
  ( InstanaSample
  , InstanaMetricValue(..)
  , SampleJson(..)
  , TimedSample(..)
  , ValueJson(..)
  , ekgSampleToInstanaSample
  , ekgValueToInstanaValue
  , empty
  , encodeSample
  , encodeValue
  , isMarkedForReset
  , markForReset
  , mkTimedSample
  , timedSampleFromEkgSample
  ) where


import qualified Data.Aeson          as Aeson
import qualified Data.Aeson.Types    as A
import           Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import           Data.Text           (Text)
import qualified Data.Text           as T
import           GHC.Generics
import qualified System.Metrics      as Metrics


-- |A collection of metric values.
type InstanaSample = HashMap Text InstanaMetricValue


-- |A single metric value.
data InstanaMetricValue =
  -- |A string metric value.
    StringValue     Text
  -- |An integral metric value.
  | IntegralValue   Int
  -- |A fractional metric value.
  | FractionalValue Double
  deriving (Eq, Generic, Show)


-- |Converts an ekg-core sample into an Instana sample.
ekgSampleToInstanaSample :: Metrics.Sample -> InstanaSample
ekgSampleToInstanaSample =
  HashMap.map ekgValueToInstanaValue


-- |Converts an ekg-core metric value into an Instana metric value.
ekgValueToInstanaValue :: Metrics.Value -> InstanaMetricValue
ekgValueToInstanaValue ekgValue =
  case ekgValue of
    Metrics.Label text     -> StringValue text
    Metrics.Counter int64  -> IntegralValue (fromIntegral int64)
    Metrics.Gauge int64    -> IntegralValue (fromIntegral int64)
    Metrics.Distribution _ -> StringValue "distribution"


-- |A metrics sample with timestamp
data TimedSample =
  TimedSample
    {
      -- |The metrics sample
      sample    :: InstanaSample
      -- |The timestamp
    , timestamp :: Int
    , resetNext :: Bool
    } deriving (Eq, Generic, Show)


-- |Creates an empty sample with a timestamp.
empty :: Int -> TimedSample
empty t =
  TimedSample {
    sample    = HashMap.empty
  , timestamp = t
  , resetNext = False
  }


-- |Creates a sample with a timestamp.
mkTimedSample :: InstanaSample -> Int -> TimedSample
mkTimedSample sampledMetrics t =
  TimedSample {
    sample    = sampledMetrics
  , timestamp = t
  , resetNext = False
  }


-- |Creates a sample with a timestamp from an ekg-core sample.
timedSampleFromEkgSample :: Metrics.Sample -> Int -> TimedSample
timedSampleFromEkgSample sampledMetrics =
  mkTimedSample (ekgSampleToInstanaSample sampledMetrics)


-- |Marks the sample for a reset on the next metric collection tick.
markForReset :: TimedSample -> TimedSample
markForReset timedSample =
  timedSample { resetNext = True }


-- |Checks if the sample is marked for reset.
isMarkedForReset :: TimedSample -> Bool
isMarkedForReset = resetNext


-- |Encodes a sample to JSON.
encodeSample :: InstanaSample -> A.Value
encodeSample metrics =
    buildOne metrics $ A.emptyObject
  where
    buildOne :: HashMap T.Text InstanaMetricValue -> A.Value -> A.Value
    buildOne m o = HashMap.foldlWithKey' build o m

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

    go :: A.Value -> [T.Text] -> InstanaMetricValue -> A.Value
    go (A.Object m) [str] val      = A.Object $ HashMap.insert str metric m
      where metric = encodeValue val
    go (A.Object m) (str:rest) val = case HashMap.lookup str m of
        Nothing -> A.Object $ HashMap.insert str (go A.emptyObject rest val) m
        Just m' -> A.Object $ HashMap.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 value to JSON
encodeValue :: InstanaMetricValue -> A.Value
encodeValue (IntegralValue   n) = Aeson.toJSON n
encodeValue (FractionalValue f) = Aeson.toJSON f
encodeValue (StringValue     s) = Aeson.toJSON s


-- |A type wrapper to convert a sample to JSON.
newtype SampleJson = SampleJson InstanaSample
    deriving Show

instance A.ToJSON SampleJson where
    toJSON (SampleJson s) = encodeSample s


-- |A type wrapper to convert a metric value to JSON.
newtype ValueJson = ValueJson InstanaMetricValue
    deriving Show

instance A.ToJSON ValueJson where
    toJSON (ValueJson v) = encodeValue v