{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ConstraintKinds #-}

module Data.Pickle ( Tags(..)
                   , StatsDConfig(..)
                   , MetricData
                   , defaultConfig
                   , withPickleDo
                   , metric
                   , gauge
                   , counter
                   , timer
                   , showT
                   )
where

import qualified Data.Map.Strict as M
import Data.Maybe
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.IO as T
import Control.Exception
import Control.Monad
import Network.Socket hiding (send)
import Network.Socket.ByteString (send)
import Control.Monad.IO.Class
import System.IO.Unsafe
import Control.Concurrent.MVar

-- | Tags for DogStatsD
type Tags = M.Map T.Text T.Text

-- | Configuration for the UDP connection used
data StatsDConfig = StatsDConfig { statsdHost      :: T.Text -- ^ Host of statsd server
                                 , statsdPort      :: T.Text -- ^ Port of statsd server
                                 , statsdPrefix    :: T.Text -- ^ Prefix concatenated to all metrics names in our program
                                 , statsdTags      :: Tags   -- ^ Mappended tags for all stats we report
                                 , statsdVerbose   :: Bool   -- ^ Whether to print all metrics to stdout
                                 }

-- | 'Pickle' is our Data Dog (get it?) and he holds on to our sock and config. 
--   `pickle` is a little MVar bed which Pickle likes to sleep in. He is a good boy.
data Pickle = Pickle { pickleSock :: Socket
                     , pickleCfg  :: StatsDConfig
                     }

-- | Something that can be sent as a metric.
type MetricData a = (Show a, Real a)

-- | Default config used for StatsD UDP connection ()
defaultConfig :: StatsDConfig
defaultConfig = StatsDConfig { statsdHost    = "127.0.0.1"
                             , statsdPort    = "8125"
                             , statsdPrefix  = ""
                             , statsdTags    = M.empty
                             , statsdVerbose = False
                             }

{-| Start up our statsd client. This can and should be attached directly to main:
> main = withPickleDo defaultConfig $ do (...)

This function can be nested, but one thread in your program at a time should be the "owner" of the pickle stack.
Other threads can use the active pickle, but they shouldn't call this function since it changes settings for all threads.
-}
withPickleDo :: StatsDConfig -> IO a -> IO a
withPickleDo cfg f = do
    mPick <- catch (Just <$> takeMVar pickle) (\(e :: BlockedIndefinitelyOnMVar) -> pure Nothing )
    case mPick of
        Nothing -> do -- First initialization.
            pick <- initPickle cfg
            bracket_
                (putMVar pickle (pick))
                (close =<< pickleSock <$> takeMVar pickle)
                (f)
        Just oldPick -> do -- Update settings.
            newPick <- initPickle cfg
            bracket_
                (putMVar pickle (newPick))
                (do
                    close =<< pickleSock <$> takeMVar pickle
                    putMVar pickle oldPick
                    )
                (f)

-- | Send a gauge.
gauge :: (MetricData a) => T.Text -> a -> Maybe Tags -> IO()
gauge name val mTags = metric "g" name val mTags Nothing
-- | alias for gauge since it can be hard to spell.
gage :: (MetricData a) => T.Text -> a -> Maybe Tags -> IO()
gage  = gauge
-- | alias for gauge since it can be hard to spell.
guage :: (MetricData a) => T.Text -> a -> Maybe Tags -> IO()
guage = gauge

-- | Send a counter.
counter :: (MetricData a) => T.Text -> a -> Maybe Tags -> Maybe Float -> IO()
counter = metric "c"

-- | Send a timer.
timer :: (MetricData a) => T.Text -> a -> Maybe Tags -> Maybe Float -> IO()
timer = metric "ms"

-- | Send a metric. Parses the options together.
metric :: (MetricData a)
      => T.Text      -- ^ metric kind in character form (g,c,ms,s)
      -> T.Text      -- ^ metric name
      -> a           -- ^ metric value
      -> Maybe Tags  -- ^ Tags for metric
      -> Maybe Float -- ^ Sampling rate for applicable metrics.
      -> IO()
metric kind n val mTags mSampling = do
    pick@(Pickle sock cfg) <- takeMVar pickle
    let tags = parseTags $ (fromMaybe M.empty mTags) <> (statsdTags cfg)
        sampling = maybe "" (\s -> "|@" <> showT s ) mSampling
        name = (statsdPrefix cfg) <> n
        msg  = name <> ":" <> (showT val) <> "|" <> kind <> sampling
    when (statsdVerbose cfg) (T.putStrLn $ "Sending metric: " <> msg)
    void $ (try $ send sock $ T.encodeUtf8 msg :: IO(Either SomeException Int))
    putMVar pickle pick

-- | Parse tags into string to send.
parseTags :: Tags -> T.Text
parseTags tags
    | M.null tags = ""
    | otherwise   = parsed where
        parsed  = "#|" <> trimmed
        trimmed = T.dropEnd 1 catted
        catted  = M.foldrWithKey (\k a b -> b <> k <> ":" <> a <> ",") "" tags

-- | Internal MVar keeping track of singleton connection.
pickle :: MVar Pickle
pickle = unsafePerformIO $ newEmptyMVar :: MVar Pickle
{-# NOINLINE pickle #-}

-- | Start the connection for our Pickle
initPickle :: StatsDConfig -> IO Pickle
initPickle cfg = do
    when (statsdVerbose cfg) $ putStrLn "Initializing Pickle StatsD Client.."
    addrinfos <- getAddrInfo Nothing (Just $ T.unpack $ statsdHost cfg) (Just $ T.unpack $ statsdPort cfg)
    let serveraddr = head addrinfos
    sock <- socket (addrFamily serveraddr) Datagram defaultProtocol
    connect sock (addrAddress serveraddr)
    pure $ Pickle sock cfg

-- | Internal utility to show something as Text
showT :: (Show a) => a -> T.Text
showT = T.pack . show