{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE OverloadedStrings #-}
module Network.Carbon.Plaintext
  ( -- * Interacting with Carbon
    -- ** Connections
    Connection(..)
  , connect
  , disconnect

    -- ** Metrics
  , sendMetrics
  , sendMetric
  , Metric(..)

    -- * Protocol details
  , encodeMetric
  )
  where

import Control.Monad (unless)
import Data.Monoid ((<>), mempty, mappend)
import Data.Typeable (Typeable)

import qualified Data.ByteString.Builder as Builder
import qualified Data.Time as Time
import qualified Data.Time.Clock.POSIX as Time
import qualified Data.Vector as V
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import qualified Network.Socket as Network
import qualified Network.Socket.ByteString.Lazy as Network

--------------------------------------------------------------------------------
-- | Low-level representation of a Carbon connection. It's suggested that you
-- construct this via 'connect'. It is henceforth assumed that
-- 'Network.getPeerName' will return something useful, which usually means
-- 'connectionSocket' should have been 'Network.connect'ed at least once.
data Connection = Connection
  { connectionSocket :: !Network.Socket
    -- ^ The connection socket to Carbon.
  }
  deriving (Eq, Show, Typeable)


--------------------------------------------------------------------------------
-- | Connect to Carbon.
connect :: Network.SockAddr -> IO Connection
connect sockAddr = fmap Connection $ do
  s <- Network.socket Network.AF_INET Network.Stream Network.defaultProtocol
  Network.connect s sockAddr
  return s


--------------------------------------------------------------------------------
-- | Disconnect from Carbon. Note that it's still valid to 'sendMetrics' to this
-- 'Connection', and it will result in a reconnection.
disconnect :: Connection -> IO ()
disconnect (Connection s) = Network.close s


--------------------------------------------------------------------------------
reconnect :: Connection -> IO ()
reconnect (Connection s) = do
  peer <- Network.getPeerName s
  Network.connect s peer


--------------------------------------------------------------------------------
-- | A single data point. A metric has a path that names it, a value, and the
-- time the metric was sampled.
data Metric = Metric
  { metricPath :: !Text.Text
  , metricValue :: !Double
  , metricTimeStamp :: !Time.UTCTime
  }
  deriving (Eq, Show, Typeable)


--------------------------------------------------------------------------------
-- | Send a collection of metrics to Carbon.
sendMetrics :: Connection -> V.Vector Metric -> IO ()
sendMetrics c ms = do
  let socket = connectionSocket c

  do isWritable <- Network.isWritable socket
     unless isWritable (reconnect c)

  Network.sendAll socket (Builder.toLazyByteString (V.foldl' mappend mempty (V.map encodeMetric ms)))

--------------------------------------------------------------------------------
-- | Send a single metric.
sendMetric :: Connection -> Text.Text -> Double -> Time.UTCTime -> IO ()
sendMetric c k v t = sendMetrics c (V.singleton (Metric k v t))


--------------------------------------------------------------------------------
-- | Encode a 'Metric' for transmission over the plain text protocol.
encodeMetric :: Metric -> Builder.Builder
encodeMetric (Metric k v t) =
  Builder.byteString (Text.encodeUtf8 k) <> " " <>
  Builder.stringUtf8 (show v) <> " " <>
  Builder.stringUtf8 (show (round (Time.utcTimeToPOSIXSeconds t) :: Int)) <> "\n"