{-# LANGUAGE DeriveGeneric     #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Implementation of basic JSON-RPC data types.
module Network.JSONRPC.Data
( -- * Requests
  Request(..)
, BatchRequest(..)
  -- ** Parsing
, FromRequest(..)
, fromRequest
  -- ** Encoding
, ToRequest(..)
, buildRequest

  -- * Responses
, Response(..)
, BatchResponse(..)
  -- ** Parsing
, FromResponse(..)
, fromResponse
  -- ** Encoding
, Respond
, buildResponse
  -- ** Errors
, ErrorObj(..)
, fromError
  -- ** Error messages
, errorParse
, errorInvalid
, errorParams
, errorMethod
, errorId

  -- * Others
, Message(..)
, Method
, Id(..)
, fromId
, Ver(..)

) where

import           Control.Applicative
import           Control.DeepSeq
import           Control.Monad
import           Data.Aeson           (encode)
import           Data.Aeson.Types
import           Data.ByteString      (ByteString)
import qualified Data.ByteString.Lazy as L
import           Data.Hashable        (Hashable)
import           Data.Maybe
import           Data.Text            (Text)
import qualified Data.Text            as T
import           Data.Text.Encoding
import           GHC.Generics         (Generic)


--
-- Requests
--

data Request = Request { getReqVer    :: !Ver
                       , getReqMethod :: !Method
                       , getReqParams :: !Value
                       , getReqId     :: !Id
                       }
             | Notif   { getReqVer    :: !Ver
                       , getReqMethod :: !Method
                       , getReqParams :: !Value
                       }
             deriving (Eq, Show, Generic)

instance NFData Request where
    rnf (Request v m p i) = rnf v `seq` rnf m `seq` rnf p `seq` rnf i
    rnf (Notif v m p)     = rnf v `seq` rnf m `seq` rnf p

instance ToJSON Request where
    toJSON (Request V2 m p i) = object $ case p of
        Null -> [jr2, "method" .= m, "id" .= i]
        _    -> [jr2, "method" .= m, "id" .= i, "params" .= p]
    toJSON (Request V1 m p i) = object $ case p of
        Null -> ["method" .= m, "params" .= emptyArray, "id" .= i]
        _    -> ["method" .= m, "params" .= p, "id" .= i]
    toJSON (Notif V2 m p) = object $ case p of
        Null -> [jr2, "method" .= m]
        _    -> [jr2, "method" .= m, "params" .= p]
    toJSON (Notif V1 m p) = object $ case p of
        Null -> ["method" .= m, "params" .= emptyArray, "id" .= Null]
        _    -> ["method" .= m, "params" .= p, "id" .= Null]

class FromRequest q where
    -- | Parser for params Value in JSON-RPC request.
    parseParams :: Method -> Maybe (Value -> Parser q)

fromRequest :: FromRequest q => Request -> Either ErrorObj q
fromRequest req =
    case parserM of
        Nothing -> Left $ errorMethod m
        Just parser ->
            case parseMaybe parser p of
                Nothing -> Left $ errorParams p
                Just  q -> Right q
  where
    m = getReqMethod req
    p = getReqParams req
    parserM = parseParams m

instance FromRequest Value where
    parseParams = const $ Just return

instance FromRequest () where
    parseParams = const . Just . const $ return ()

instance FromJSON Request where
    parseJSON = withObject "request" $ \o -> do
        (v, n, m, p) <- parseVerIdMethParams o
        case n of Nothing -> return $ Notif   v m p
                  Just i  -> return $ Request v m p i

parseVerIdMethParams :: Object -> Parser (Ver, Maybe Id, Method, Value)
parseVerIdMethParams o = do
    v <- parseVer o
    i <- o .:? "id"
    m <- o .: "method"
    p <- o .:? "params" .!= Null
    return (v, i, m, p)

class ToRequest q where
    -- | Method associated with request data to build a request object.
    requestMethod :: q -> Method

    -- | Is this request to be sent as a notification (no id, no response)?
    requestIsNotif :: q -> Bool

instance ToRequest Value where
    requestMethod = const "json"
    requestIsNotif = const False

instance ToRequest () where
    requestMethod = const "json"
    requestIsNotif = const False

buildRequest :: (ToJSON q, ToRequest q)
             => Ver             -- ^ JSON-RPC version
             -> q               -- ^ Request data
             -> Id
             -> Request
buildRequest ver q = if requestIsNotif q
                         then const $ Notif ver (requestMethod q) (toJSON q)
                         else Request ver (requestMethod q) (toJSON q)

--
-- Responses
--

data Response = Response      { getResVer :: !Ver
                              , getResult :: !Value
                              , getResId  :: !Id
                              }
              | ResponseError { getResVer :: !Ver
                              , getError  :: !ErrorObj
                              , getResId  :: !Id
                              }
              | OrphanError   { getResVer :: !Ver
                              , getError  :: !ErrorObj
                              }
              deriving (Eq, Show, Generic)


instance NFData Response where
    rnf (Response v r i)      = rnf v `seq` rnf r `seq` rnf i
    rnf (ResponseError v o i) = rnf v `seq` rnf o `seq` rnf i
    rnf (OrphanError v o)     = rnf v `seq` rnf o

instance ToJSON Response where
    toJSON (Response V1 r i) = object
        ["id" .= i, "result" .= r, "error" .= Null]
    toJSON (Response V2 r i) = object
        [jr2, "id" .= i, "result" .= r]
    toJSON (ResponseError V1 e i) = object
        ["id" .= i, "error" .= e, "result" .= Null]
    toJSON (ResponseError V2 e i) = object
        [jr2, "id" .= i, "error" .= e]
    toJSON (OrphanError V1 e) = object
        ["id" .= Null, "error" .= e, "result" .= Null]
    toJSON (OrphanError V2 e) = object
        [jr2, "id" .= Null, "error" .= e]

class FromResponse r where
    -- | Parser for result Value in JSON-RPC response.
    -- Method corresponds to request to which this response answers.
    parseResult :: Method -> Maybe (Value -> Parser r)

-- | Parse a response knowing the method of the corresponding request.
fromResponse :: FromResponse r => Method -> Response -> Maybe r
fromResponse m (Response _ r _) = parseResult m >>= flip parseMaybe r
fromResponse _ _                = Nothing

instance FromResponse Value where
    parseResult = const $ Just return

instance FromResponse () where
    parseResult = const Nothing

instance FromJSON Response where
    parseJSON = withObject "response" $ \o -> do
        (v, d, s) <- parseVerIdResultError o
        case s of
            Right r -> do
                guard $ isJust d
                return $ Response v r (fromJust d)
            Left e ->
                case d of
                    Just  i -> return $ ResponseError v e i
                    Nothing -> return $ OrphanError v e

parseVerIdResultError :: Object
                      -> Parser (Ver, Maybe Id, Either ErrorObj Value)
parseVerIdResultError o = do
    v <- parseVer o
    i <- o .:? "id"
    r <- o .:? "result" .!= Null
    p <- case v of
          V1 -> if r == Null then Left <$> o .: "error" else return $ Right r
          V2 -> maybe (Right r) Left <$> o .:? "error"
    return (v, i, p)

-- | Create a response from a request. Use in servers.
buildResponse :: (Monad m, FromRequest q, ToJSON r)
              => Respond q m r
              -> Request
              -> m (Maybe Response)
buildResponse f req@(Request v _ _ i) =
    case fromRequest req of
        Left e -> return . Just $ ResponseError v e i
        Right q -> do
            rE <- f q
            case rE of
                Left  e -> return . Just $ ResponseError v e i
                Right r -> return . Just $ Response v (toJSON r) i
buildResponse _ _ = return Nothing

-- | Type of function to make it easy to create a response from a request.
-- Meant to be used in servers.
type Respond q m r = q -> m (Either ErrorObj r)

-- | Error object from JSON-RPC 2.0. ErrorVal for backwards compatibility.
data ErrorObj = ErrorObj  { getErrMsg  :: !String
                          , getErrCode :: !Int
                          , getErrData :: !Value
                          }
              | ErrorVal  { getErrData :: !Value }
              deriving (Show, Eq, Generic)

instance NFData ErrorObj where
    rnf (ErrorObj m c d) = rnf m `seq` rnf c `seq` rnf d
    rnf (ErrorVal v)     = rnf v

instance FromJSON ErrorObj where
    parseJSON Null = mzero
    parseJSON v@(Object o) = p1 <|> p2 where
        p1 = do
            m <- o .: "message"
            c <- o .: "code"
            d <- o .:? "data" .!= Null
            return $ ErrorObj m c d
        p2 = return $ ErrorVal v
    parseJSON v = return $ ErrorVal v

instance ToJSON ErrorObj where
    toJSON (ErrorObj s i d) = object $ ["message" .= s, "code" .= i]
        ++ if d == Null then [] else ["data" .= d]
    toJSON (ErrorVal v) = v

-- | Get a user-friendly string with the error information.
fromError :: ErrorObj -> String
fromError (ErrorObj m c v) = show c ++ ": " ++ m ++ ": " ++ valueAsString v
fromError (ErrorVal (String t)) = T.unpack t
fromError (ErrorVal v) = valueAsString v

valueAsString :: Value -> String
valueAsString = T.unpack . decodeUtf8 . L.toStrict . encode

-- | Parse error.
errorParse :: ByteString -> ErrorObj
errorParse = ErrorObj "Parse error" (-32700) . String . decodeUtf8

-- | Invalid request.
errorInvalid :: Value -> ErrorObj
errorInvalid = ErrorObj "Invalid request" (-32600)

-- | Invalid params.
errorParams :: Value -> ErrorObj
errorParams = ErrorObj "Invalid params" (-32602)

-- | Method not found.
errorMethod :: Method -> ErrorObj
errorMethod = ErrorObj "Method not found" (-32601) . toJSON

-- | Id not recognized.
errorId :: Id -> ErrorObj
errorId = ErrorObj "Id not recognized" (-32000) . toJSON


--
-- Messages
--

data BatchRequest
    = BatchRequest     { getBatchRequest  :: ![Request] }
    | SingleRequest    { getSingleRequest ::  !Request  }
    deriving (Eq, Show, Generic)

instance NFData BatchRequest where
    rnf (BatchRequest qs) = rnf qs
    rnf (SingleRequest q) = rnf q

instance FromJSON BatchRequest where
    parseJSON qs@Array{} = BatchRequest  <$> parseJSON qs
    parseJSON q@Object{} = SingleRequest <$> parseJSON q
    parseJSON _          = mzero

instance ToJSON BatchRequest where
    toJSON (BatchRequest qs) = toJSON qs
    toJSON (SingleRequest q) = toJSON q

data BatchResponse
    = BatchResponse    { getBatchResponse :: ![Response] }
    | SingleResponse   { getSingleResponse :: !Response  }
    deriving (Eq, Show, Generic)

instance NFData BatchResponse where
    rnf (BatchResponse qs) = rnf qs
    rnf (SingleResponse q) = rnf q

instance FromJSON BatchResponse where
    parseJSON qs@Array{} = BatchResponse  <$> parseJSON qs
    parseJSON q@Object{} = SingleResponse <$> parseJSON q
    parseJSON _          = mzero

instance ToJSON BatchResponse where
    toJSON (BatchResponse qs) = toJSON qs
    toJSON (SingleResponse q) = toJSON q

data Message
    = MsgRequest  { getMsgRequest  :: !Request   }
    | MsgResponse { getMsgResponse :: !Response  }
    | MsgBatch    { getBatch       :: ![Message] }
    deriving (Eq, Show, Generic)

instance NFData Message where
    rnf (MsgRequest  q) = rnf q
    rnf (MsgResponse r) = rnf r
    rnf (MsgBatch    b) = rnf b

instance ToJSON Message where
    toJSON (MsgRequest  q) = toJSON q
    toJSON (MsgResponse r) = toJSON r
    toJSON (MsgBatch    b) = toJSON b

instance FromJSON Message where
    parseJSON v = (MsgRequest   <$> parseJSON v)
              <|> (MsgResponse  <$> parseJSON v)
              <|> (MsgBatch     <$> parseJSON v)

--
-- Types
--

type Method = Text

data Id = IdInt { getIdInt :: !Int  }
        | IdTxt { getIdTxt :: !Text }
    deriving (Eq, Show, Read, Generic)

instance Hashable Id

instance NFData Id where
    rnf (IdInt i) = rnf i
    rnf (IdTxt t) = rnf t

instance Enum Id where
    toEnum = IdInt
    fromEnum (IdInt i) = i
    fromEnum _         = error "Can't enumerate non-integral ids"

instance FromJSON Id where
    parseJSON s@(String _) = IdTxt <$> parseJSON s
    parseJSON n@(Number _) = IdInt <$> parseJSON n
    parseJSON _            = mzero

instance ToJSON Id where
    toJSON (IdTxt s) = toJSON s
    toJSON (IdInt n) = toJSON n

-- | Pretty display a message id. Meant for logs.
fromId :: Id -> String
fromId (IdInt i) = show i
fromId (IdTxt t) = T.unpack t

-- | JSON-RPC version.
data Ver = V1 -- ^ JSON-RPC 1.0
         | V2 -- ^ JSON-RPC 2.0
         deriving (Eq, Show, Read, Generic)

instance NFData Ver where
    rnf v = v `seq` ()

jr2 :: Pair
jr2 = "jsonrpc" .= ("2.0" :: Text)

parseVer :: Object -> Parser Ver
parseVer o = do
    j <- o .:? "jsonrpc"
    return $ if j == Just ("2.0" :: Text) then V2 else V1
