{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}

module Xmobar.Plugins.Kraken (Kraken(..)) where

import Control.Concurrent (MVar, newEmptyMVar, putMVar, takeMVar)
import Control.Concurrent.Async (async, cancel)
import Control.Exception (bracket, catch)
import Control.Monad (forever, mzero, void, when)
import Data.Aeson
import Data.Aeson.Types (Parser, typeMismatch)
import Data.List (sort)
import Data.Text (Text, pack, unpack)
import GHC.Generics
import Network.WebSockets (ClientApp, ConnectionException(ConnectionClosed), receiveData, sendTextData)
import System.IO (hPutStrLn, stderr)
import Text.Read (readMaybe)
import Wuss (runSecureClient)
import Xmobar.Run.Exec(Exec(..))

import qualified Data.HashMap.Lazy as HML (lookup)
import qualified Data.Map as Map
import qualified Data.Vector as V

data Kraken = Kraken [String] String
  deriving (Read, Show)

instance Exec Kraken where
  alias (Kraken _ a) = a
  start (Kraken ps _) cb = do
    mvar <- newEmptyMVar
    bracket (async $ reconnectOnConnectionClose $ wsClientApp ps mvar) cancel $ \_ -> do
      let loop mv p = do
           v <- takeMVar mv
           let g = Map.insert (unpack $ fst v) (snd v) p
           cb (display g)
           loop mv g

      loop mvar (Map.fromList $ map (, 0.0) ps)

    where
      display :: Map.Map String Double -> String
      display m = unwords $ sort $ map (\x -> fst x ++ ": " ++ show (snd x)) $ Map.toList m

      reconnectOnConnectionClose :: ClientApp () -> IO ()
      reconnectOnConnectionClose ws = runSecureClient "ws.kraken.com" 443 "/" ws
        `catch` (\e -> when (e == ConnectionClosed) $ reconnectOnConnectionClose ws)

wsClientApp :: [String] -> MVar (Text, Double)  -> ClientApp ()
wsClientApp ps mvar connection = do
  sendTextData connection (encode Subscribe { event = "subscribe", pair = map pack ps, subscription = Subscription { name = "ticker" }})
  void . forever $ do
    message <- receiveData connection
    case (eitherDecode message :: Either String Message) of
      Right m ->
        case m of
          TickerMessage _ ti _ tp  -> putMVar mvar (tp, askPrice $ ask ti)
          _ -> return ()
      Left e -> hPutStrLn stderr e

data Ask = Ask {
    askPrice :: Double
  , askWholeLotVolume :: Int
  , askLotVolume :: Double
  } deriving Show

parseDoubleString :: Value -> Parser Double
parseDoubleString v = do
  j <- parseJSON v
  case readMaybe j of
    Just num -> return num
    Nothing -> typeMismatch "Double inside a String" v

instance FromJSON Ask where
  parseJSON (Array v)
    | V.length v == 3 = do
      p <- parseDoubleString $ v V.! 0
      w <- parseJSON $ v V.! 1
      l <- parseDoubleString $ v V.! 2
      return Ask { askPrice = p, askWholeLotVolume = w, askLotVolume = l }
    | otherwise = mzero
  parseJSON nonArray = typeMismatch "Array" nonArray

data Bid = Bid {
    bidPrice :: Double
  , bidWholeLotVolume :: Int
  , bidLotVolume :: Double
  } deriving Show

instance FromJSON Bid where
  parseJSON (Array v)
    | V.length v == 3 = do
      p <- parseDoubleString $ v V.! 0
      w <- parseJSON $ v V.! 1
      l <- parseDoubleString $ v V.! 2
      return Bid { bidPrice = p, bidWholeLotVolume = w, bidLotVolume = l }
    | otherwise = mzero
  parseJSON nonArray = typeMismatch "Array" nonArray

data Close = Close {
    closePrice :: Double
  , closeLotVolume :: Double
  } deriving Show

instance FromJSON Close where
  parseJSON (Array v)
    | V.length v == 2 = do
      p <- parseDoubleString $ v V.! 0
      l <- parseDoubleString $ v V.! 1
      return Close { closePrice= p, closeLotVolume = l }
    | otherwise = mzero
  parseJSON nonArray = typeMismatch "Array" nonArray

data TickerInformation = TickerInformation {
    ask :: Ask
  , bid :: Bid
  , close :: Close
  } deriving Show

instance FromJSON TickerInformation where
  parseJSON = withObject "P" $ \v -> TickerInformation
    <$> v .: "a"
    <*> v .: "b"
    <*> v .: "c"

data Message =
    Heartbeat
  | TickerMessage { channelId :: Int, tickerInformation :: TickerInformation, channelName :: Text, tickerPair :: Text }
  | SubscriptionStatus { channelName :: Text, status :: Text, subscriptionPair :: Text }
  | SystemStatus { connectionId :: Integer, status :: Text, version :: Text }
  | UnrecognizedMessage String
  deriving Show

newtype Subscription = Subscription { name :: Text } deriving (Generic, Show)
instance ToJSON Subscription where
  toEncoding = genericToEncoding defaultOptions

data Subscribe = Subscribe { event :: Text, pair :: [Text], subscription :: Subscription } deriving (Generic, Show)
instance ToJSON Subscribe where
  toEncoding = genericToEncoding defaultOptions

instance FromJSON Message where
  parseJSON (Object o) = case HML.lookup (pack "event") o of
    Just (String "heartbeat") -> pure Heartbeat
    Just (String "systemStatus") -> systemStatus o
    Just (String "subscriptionStatus") -> subscriptionStatus o
    Just eventType -> pure $ UnrecognizedMessage $ "Unrecognized event type " ++ show eventType
    Nothing -> pure $ UnrecognizedMessage "Missing event"
    where
      systemStatus obj = SystemStatus <$> obj .: "connectionID" <*> obj .: "status" <*> obj .: "version"
      subscriptionStatus obj = SubscriptionStatus <$> obj .: "channelName" <*> obj .: "status" <*> obj .: "pair"
  parseJSON (Array a)
    | V.length a == 4 = do
      cId   <- parseJSON $ a V.! 0
      info  <- parseJSON $ a V.! 1
      cName <- parseJSON $ a V.! 2
      p     <- parseJSON $ a V.! 3
      pure TickerMessage { channelId = cId, tickerInformation = info, channelName = cName, tickerPair = p }
    | otherwise = mzero
  parseJSON v = typeMismatch "Object or Array" v
