{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}

{-|
Module:      Control.Remote.Monad.JSON.Router where
Copyright:   (C) 2015, The University of Kansas
License:     BSD-style (see the file LICENSE)
Maintainer:  Justin Dawson
Stability:   Alpha
Portability: GHC
-}

module Control.Remote.Monad.JSON.Router 
        ( -- * The server RPC router
          router
          -- * The datatype that represents what we receive and what we dispatch
        , ReceiveAPI(..)
        , Call(..)
          -- * Utilty methods
        , transport
        , methodNotFound
        , invalidParams
        , parseError        
        ) where
        
import           Control.Monad.Catch
import           Control.Remote.Monad.JSON.Types
import           Control.Natural

import           Data.Aeson
import           Data.Text(Text)
import           Data.Typeable
import qualified Data.Vector as V

-- | 'Call' is a user-visable deep embedding of a method or notification call.
-- Server's provide transformations on this to implement remote-side call dispatching.
data Call :: * -> * where
  CallMethod       :: Text -> Args -> Call Value
  CallNotification :: Text -> Args -> Call ()

-- | "The Server MAY process a batch rpc call as a set of concurrent tasks,
--    processing them in any order and with any width of parallelism."
--   We control this using the first argument.         
router :: MonadCatch m 
       => (forall a. [m a] -> m [a])
       -> (Call :~> m) -> (ReceiveAPI :~> m)
router s f = nat $ \ case
  (Receive v@(Object {})) -> simpleRouter f v
  (Receive (Array a)) 
    | V.null a -> return $ Just $ invalidRequest
    | otherwise -> do
          rs <- s (map (simpleRouter f) $ V.toList a)
          case [ v | Just v <- rs ] of
            [] -> return Nothing -- If there are no Response objects contained within the
                                 -- Response array as it is to be sent to the client,
                                 -- the server MUST NOT return an empty Array and should
                                 -- return nothing at all.
            vs -> return (Just (toJSON vs))
  (Receive _) -> return $ Just $ invalidRequest
        
-- The simple router handle a single call.
simpleRouter :: forall m . MonadCatch m 
       => (Call :~> m) 
       -> Value -> m (Maybe Value)
simpleRouter (Nat f) v = case call <$> fromJSON v of
    Success m -> m
    Error _ ->  return $ Just $ invalidRequest
  where
        call :: JSONCall -> m (Maybe Value)
        call (MethodCall (Method nm args) tag) = (do
                r <- f (CallMethod nm args :: Call Value)
                return $ Just $ object
                       [ "jsonrpc" .= ("2.0" :: Text)
                       , "result" .= toJSON r
                       , "id" .= tag
                       ]) `catches` 
                          [ Handler $ \ (_ :: MethodNotFound) -> 
                               return $ Just $ toJSON 
                                      $ errorResponse (-32601) "Method not found" tag
                          , Handler $ \ (_ :: InvalidParams) -> 
                               return $ Just $ toJSON 
                                      $ errorResponse (-32602) "Invalid params" tag
                          , Handler $ \ (_ :: SomeException) ->
                               return $ Just $ toJSON 
                                      $ errorResponse (-32603) "Internal error" tag                                
                          ]
        call (NotificationCall (Notification nm args)) =
            (f (CallNotification nm args) >> return Nothing) `catchAll` \ _ -> return Nothing

-- | 'transport' connects the ability to recieve a message with the ability
-- to send a message. Typically this is done using TCP/IP and HTTP,
-- but we can simulate the connection here.

transport :: (Monad f) => (ReceiveAPI :~> f) -> (SendAPI :~> f)
transport f = nat $ \ case
  Sync v -> do
    r <- f # Receive v
    case r of
      Nothing -> fail "no result returned in transport"
      Just v0 -> return v0
  Async v -> do
    r <- f # Receive v
    case r of
      Nothing -> return ()
      Just v0 -> fail $ "unexpected result in transport: " ++ show v0


errorResponse :: Int -> Text -> Value -> Value
errorResponse code msg theId = toJSON $
        ErrorResponse (ErrorMessage code msg) theId

invalidRequest :: Value
invalidRequest = errorResponse (-32600) "Invalid Request" Null


-- | For use when parsing to a JSON value fails inside a server,
--   before calling the router
parseError :: Value
parseError = errorResponse (-32700) "Parse error" Null

data MethodNotFound = MethodNotFound 
  deriving (Show, Typeable)

instance Exception MethodNotFound

-- | Throw this exception when a 'JSONCall a -> IO a' fails to match a method
--   or notification.
methodNotFound :: MonadThrow m => m a
methodNotFound = throwM $ MethodNotFound

data InvalidParams = InvalidParams 
  deriving (Show, Typeable)

instance Exception InvalidParams

-- | Throw this for when a 'JSONCall a -> IO a' method matches, but has invalid params.
invalidParams :: MonadThrow m => m a
invalidParams = throwM $ InvalidParams