{-|

Description: Intended to provide a set of callbacks for various server events.

This module is intended to provide a set of callbacks for various server events.

The set of callbacks provided fulfills the functionality require to be able
to implement a local data-store reflecting server-sent data-update messages.

"Web.DDP.Deadpan.Callbacks" is used frequently in "Web.DDP.Deadpan".

-}

{-# LANGUAGE OverloadedStrings #-}

module Web.DDP.Deadpan.Callbacks where

import Web.DDP.Deadpan.DSL
import Control.Concurrent.MVar
import Control.Monad.State
import Data.Monoid

-- Old Stuff...

-- Client -->> Server

-- Client Heartbeat

pingCallback :: Callback
pingCallback = sendMessage "pong"
             . maybe (ejobject []) makeEJsonId
             . ejson2guid

-- Client Data Subscriptions


{- |

Initiate a subscription to a named collection on the server.

Provide an id to refer to the subscription in future.

@
  sub (client -> server):
    id:     string                        (an arbitrary client-determined
                                              identifier for this subscription)
    name:   string                        (the name of the subscription)
    params: optional array of EJSON items (parameters to the subscription)
@

-}
clientDataSub :: GUID -> Text -> [ EJsonValue ] -> DeadpanApp GUID
clientDataSub subid name params = do
  sendMessage "sub" $ makeEJsonId subid
                     <> ejobject [ ("name",   ejstring name)
                                 , ("params", ejarray  params) ]
  return subid

-- | Activates a subscription with an auto-generated ID, returning the ID.
--
subscribe :: Text -> [ EJsonValue ] -> DeadpanApp GUID
subscribe name params = newID >>= \guid -> clientDataSub guid name params

subscribeWaitId :: Text -> [EJsonValue] -> DeadpanApp (Either EJsonValue (GUID, EJsonValue))
subscribeWaitId name params = do
  mv         <- liftIO newEmptyMVar
  subId      <- newID
  handlerIdL <- setMatchHandler (guid2NoSub    subId) (handlerL mv)
  handlerIdR <- setMatchHandler (guid2SubReady subId) (handlerR subId mv)
  _          <- clientDataSub subId name params
  res        <- liftIO $ readMVar mv

  -- Note: This occurs after reading the MVar so it should be safe.
  deleteHandlerID handlerIdR
  deleteHandlerID handlerIdL
  return res

  where
  -- {"msg":"ready","subs":["849d1899-f3af-44b9-919c-7a1ca72c8857"]}
  handlerR subId mv itm = liftIO $ putMVar mv $ Right (subId, itm)
  -- {"error":{...},"msg":"nosub","id":"af0a7ce1-3c37-40d7-8875-b8e3dd737765"}
  handlerL mv itm = forOf_ (_EJObjectKey "error"  . _Just) itm $ liftIO . putMVar mv . Left

subscribeWait :: Text -> [EJsonValue] -> DeadpanApp (Either EJsonValue EJsonValue)
subscribeWait name params = fmap (right' snd)
                                 (subscribeWaitId name params)


{- |
Unsubscribe from an existing subscription indicated by its ID.

@
  unsub (client -> server):
    id: string (the id passed to 'sub')
@
-}
clientDataUnsub :: GUID -> DeadpanApp ()
clientDataUnsub subid = sendMessage "unsub" (makeEJsonId subid)

-- | Synonym for `clientDataUnsub`
unsubscribe :: GUID -> DeadpanApp ()
unsubscribe = clientDataUnsub


-- Client RPC

{- |
  As explained in the Meteor DDP documentation:

  @
      method:     string                        (method name)
      params:     optional array of EJSON items (parameters to the method)
      id:         string                        (an arbitrary client-determined identifier for this method call)
      randomSeed: optional JSON value           (an arbitrary client-determined seed for pseudo-random generators)
  @
-}
clientRPCMethod :: Text -> [EJsonValue] -> GUID -> Maybe Text -> DeadpanApp GUID
clientRPCMethod method params rpcid seed = do
  let msg = [ ("method", ejstring method)
            , ("params", ejarray  params) ]
         &~ (forOf_ _Just seed   $ \v -> modify (("seed",   ejstring v):))

  sendMessage "method" (makeEJsonId rpcid <> ejobject msg)
  return rpcid

rpc :: Text -> [EJsonValue] -> DeadpanApp GUID
rpc method params = newID >>= \guid -> clientRPCMethod method params guid Nothing

-- | Like clientRPCMethod, except that it blocks, returning the response from the server.
--
-- TODO: Should we use the seed?
--
rpcWait :: Text -> [EJsonValue] -> DeadpanApp (Either EJsonValue EJsonValue)
rpcWait method params = do
  mv         <- liftIO newEmptyMVar
  rpcId      <- newID -- Can't use rpc, since we need the ID to set handlers first...
  handlerId  <- setMatchHandler (makeEJsonId rpcId) (handler mv)
  _          <- clientRPCMethod method params rpcId Nothing
  res        <- liftIO $ readMVar mv

  deleteHandlerID handlerId -- Note: This occurs after reading the MVar so it should be safe.
  return res

  where
  handler mv itm = do forOf_ (_EJObjectKey "error"  . _Just) itm $ liftIO . putMVar mv . Left
                      forOf_ (_EJObjectKey "result" . _Just) itm $ liftIO . putMVar mv . Right

-- Server -->> Client

-- Server Errors

-- TODO
-- serverError :: Callback
-- serverError = undefined