{-# LANGUAGE CPP #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE OverloadedStrings #-}

-- | Functions for implementing the server side of JSON-RPC 2.0.
--   See <http://www.jsonrpc.org/specification>.
module Network.JsonRpc.Server (
                          -- ** Instructions
                          -- $instructions

                          -- ** Requests
                          -- $requests

                          -- ** Example
                          -- $example

                          -- ** Methods
                             RpcResult
                           , Method
                           , toMethod
                           , call
                           , callWithBatchStrategy
                           , Parameter(..)
                           , (:+:) (..)
                           , MethodParams
                          -- ** Errors
                           , RpcError (..)
                           , rpcError
                           , rpcErrorWithData
                           -- ** Deprecated
                           , Methods
                           , toMethods) where

import Network.JsonRpc.Types
import Data.Text (Text, append, pack)
import Data.Maybe (catMaybes)
import qualified Data.ByteString.Lazy as B
import qualified Data.Aeson as A
import qualified Data.Vector as V
import qualified Data.HashMap.Strict as H
import Control.DeepSeq (NFData)
import Control.Monad (liftM, (<=<))
import Control.Monad.Identity (runIdentity)
import Control.Monad.Except (runExceptT, throwError)

#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>))
#endif

-- $instructions
-- * Create methods by calling 'toMethod' and providing the method
--   names, lists of parameters, and functions to be called.
--
-- * Process a request by calling 'call' or 'callWithBatchStrategy'
--   on the 'Method's and input 'B.ByteString'.

-- $requests
-- This library handles by-name and by-position arguments, batch and
-- single requests, and notifications.  It also allows each
-- parameter of a method to be either optional (with a default value)
-- or required.  The function is called as long as all required
-- arguments are present.  A request providing more positional
-- arguments than the total number of optional and required
-- parameters to a function results in an error.  However, additional
-- by-name arguments are ignored.

-- $example
-- Here is an example with three JSON-RPC methods that all have
-- access to an 'MVar' counter.  The program reads requests from
-- stdin and writes responses to stdout. Compile it with the build
-- flag @demo@.
--   
-- > {-# LANGUAGE OverloadedStrings #-}
-- > 
-- > module Main (main) where
-- > 
-- > import Network.JsonRpc.Server
-- > import System.IO (BufferMode (LineBuffering), hSetBuffering, stdout)
-- > import qualified Data.ByteString.Lazy.Char8 as B
-- > import Data.List (intercalate)
-- > import Data.Maybe (fromMaybe)
-- > import Control.Monad (forM_, when)
-- > import Control.Monad.Trans (liftIO)
-- > import Control.Monad.Except (throwError)
-- > import Control.Monad.Reader (ReaderT, ask, runReaderT)
-- > import Control.Concurrent.MVar (MVar, newMVar, modifyMVar)
-- > 
-- > main = do
-- >   hSetBuffering stdout LineBuffering
-- >   contents <- B.getContents
-- >   count <- newMVar 0
-- >   forM_ (B.lines contents) $ \request -> do
-- >          response <- runReaderT (call methods request) count
-- >          B.putStrLn $ fromMaybe "" response
-- > 
-- > type Server = ReaderT (MVar Integer) IO
-- > 
-- > methods :: [Method Server]
-- > methods = [add, printSequence, increment]
-- > 
-- > add = toMethod "add" f (Required "x" :+: Required "y" :+: ())
-- >     where f :: Double -> Double -> RpcResult Server Double
-- >           f x y = liftIO $ return (x + y)
-- > 
-- > printSequence = toMethod "print_sequence" f params
-- >     where params = Required "string" :+:
-- >                    Optional "count" 1 :+:
-- >                    Optional "separator" ',' :+: ()
-- >           f :: String -> Int -> Char -> RpcResult Server ()
-- >           f str count sep = do
-- >               when (count < 0) $ throwError negativeCount
-- >               liftIO $ print $ intercalate [sep] $ replicate count str
-- >           negativeCount = rpcError (-32000) "negative count"
-- > 
-- > increment = toMethod "increment_and_get_count" f ()
-- >     where f :: RpcResult Server Integer
-- >           f = ask >>= \count -> liftIO $ modifyMVar count inc
-- >               where inc x = return (x + 1, x + 1)
--   

-- | Creates a method from a name, function, and parameter descriptions.
--   The parameter names must be unique.
toMethod :: (MethodParams f p m r) => Text -> f -> p -> Method m
toMethod name f params = let f' args = A.toJSON `liftM` _apply f params args
                         in Method name f'

type Methods m = [Method m]
{-# DEPRECATED Methods "Use ['Method' m]." #-}

toMethods :: [Method m] -> Methods m
toMethods = id
{-# DEPRECATED toMethods "Use 'call' directly." #-}

type MethodMap m = H.HashMap Text (Method m)

-- | Handles one JSON-RPC request. It is the same as
--   @callWithBatchStrategy sequence@.
call :: Monad m => [Method m]  -- ^ Choice of methods to call.
     -> B.ByteString           -- ^ JSON-RPC request.
     -> m (Maybe B.ByteString) -- ^ The response wrapped in 'Just', or
                               --   'Nothing' in the case of a notification,
                               --   all wrapped in the given monad.
call = callWithBatchStrategy sequence

-- | Handles one JSON-RPC request. The method names must be unique.
callWithBatchStrategy :: Monad m =>
                         (forall a . NFData a => [m a] -> m [a]) -- ^ Function specifying the
                                                                 --   evaluation strategy.
                      -> [Method m]                              -- ^ Choice of methods to call.
                      -> B.ByteString                            -- ^ JSON-RPC request.
                      -> m (Maybe B.ByteString)                  -- ^ The response wrapped in 'Just', or
                                                                 --   'Nothing' in the case of a notification,
                                                                 --   all wrapped in the given monad.
callWithBatchStrategy strategy methods =
    mthMap `seq` either returnErr callMethod . parse
  where
    mthMap = H.fromList $
             map (\mth@(Method name _) -> (name, mth)) methods
    parse :: B.ByteString -> Either RpcError (Either A.Value [A.Value])
    parse = runIdentity . runExceptT . parseVal <=< parseJson
    parseJson = maybe invalidJson return . A.decode
    parseVal val =
        case val of
          obj@(A.Object _) -> return $ Left obj
          A.Array vec | V.null vec -> throwInvalidRpc "Empty batch request"
                      | otherwise -> return $ Right $ V.toList vec
          _ -> throwInvalidRpc "Not a JSON object or array"
    callMethod rq =
        case rq of
          Left val -> encodeJust `liftM` singleCall mthMap val
          Right vals -> encodeJust `liftM` batchCall strategy mthMap vals
      where
        encodeJust r = A.encode <$> r
    returnErr = return . Just . A.encode . nullIdResponse
    invalidJson = throwError $ rpcError (-32700) "Invalid JSON"

singleCall :: Monad m => MethodMap m -> A.Value -> m (Maybe Response)
singleCall methods val = case parsed of
                                Left err -> return $ nullIdResponse err
                                Right (Request name args i) ->
                                  toResponse i `liftM` runExceptT (applyMethodTo args =<< method)
                                    where method = lookupMethod name methods
    where parsed = runIdentity $ runExceptT $ parseValue val
          applyMethodTo args (Method _ f) = f args

nullIdResponse :: RpcError -> Maybe Response
nullIdResponse err = toResponse (Just IdNull) (Left err :: Either RpcError ())

parseValue :: (A.FromJSON a, Monad m) => A.Value -> RpcResult m a
parseValue val = case A.fromJSON val of
                   A.Error msg -> throwInvalidRpc $ pack msg
                   A.Success x -> return x

lookupMethod :: Monad m => Text -> MethodMap m -> RpcResult m (Method m)
lookupMethod name = maybe notFound return . H.lookup name
    where notFound = throwError $ rpcError (-32601) $ "Method not found: " `append` name

throwInvalidRpc :: Monad m => Text -> RpcResult m a
throwInvalidRpc = throwError . rpcErrorWithData (-32600) "Invalid JSON-RPC 2.0 request"

batchCall :: Monad m => (forall a. NFData a => [m a] -> m [a])
          -> MethodMap m
          -> [A.Value]
          -> m (Maybe [Response])
batchCall strategy methods vals = (noNull . catMaybes) `liftM` results
    where results = strategy $ map (singleCall methods) vals
          noNull rs = if null rs then Nothing else Just rs

toResponse :: A.ToJSON a => Maybe Id -> Either RpcError a -> Maybe Response
toResponse (Just i) r = Just $ Response i $ A.toJSON <$> r
toResponse Nothing _ = Nothing
