-- Copyright (c) 2013, GREE, Inc. All rights reserved.
-- authors: Kiyoshi Ikehara <kiyoshi.ikehara@gree.net>

{-# LANGUAGE OverloadedStrings #-}

{-|
  This module represents memcached response messages.
-}
module Network.Memcache.Response (
    Response (
        Ok
      , Value
      , End
      , Stored
      , NotStored
      , Exists
      , NotFound
      , Deleted
      , Found
      , Touched
      , Error
      , ServerError
      , ClientError
      , Version
      , Code
      , Stat)
  , parseResponse
  , parseResponseHeader
  , responseParser
  , responseHeaderParser
  ) where

import Prelude hiding (takeWhile, take)
import qualified Data.ByteString.Char8 as BS
import Data.Word
import qualified Data.Attoparsec.ByteString as AB
import Data.Attoparsec.ByteString.Char8
import qualified Data.Attoparsec.ByteString.Lazy as AL
import Control.Applicative
import Control.Monad.IO.Class

-- import Debug.Trace

import Network.Memcache.Class
import Network.Memcache.IO.Internal

instance Message Response where
  parseHeader = parseResponseHeader

  toChunks = Network.Memcache.Response.toChunks

  recvContent handle resp = case resp of
    Value key flags len _ version -> liftIO $ do
      value <- readBytes handle len
      _term <- BS.hGetLine handle
      return $ Just $ Value key flags len value version
    _ -> return $ Just resp

{-|
  response messages from memcached server
-}
data Response =
    Ok
  | Value {
      _resKey     :: !BS.ByteString
    , _resFlag    :: !Word32
    , _resLen     :: !Word64
    , _resValue   :: BS.ByteString
    , _resVersion :: !(Maybe Word64)
    }
  | End
  | Stored
  | NotStored
  | Exists
  | NotFound
  | Deleted
  | Found
  | Touched
  | Error
  | ServerError String
  | ClientError String
  | Version BS.ByteString
  | Stat BS.ByteString BS.ByteString -- name and value pair
  | Code Word64
  deriving (Show, Eq)

{-|
  Response parser by attoparsec.
-}
responseParser :: Parser Response
responseParser = responseParser' False

{-|
  Response header parser by attoparsec.
-}
responseHeaderParser :: Parser Response
responseHeaderParser = responseParser' True

-- make a parser depending on onlyHeadler switch
responseParser' :: Bool -> Parser Response
responseParser' onlyHeader = try parser <|> codeParser
  where
    parser :: Parser Response
    parser = do
      cmd <- ws *> word <* ws
      resp <- case {- trace ("cmd: " ++ show cmd) -} cmd of
        "VALUE"        -> do
          key     <- skipWhile (== ' ') *> takeWhile1 (\c -> c /= ' ')
          flags   <- skipWhile (== ' ') *> decimal
          len     <- skipWhile (== ' ') *> decimal :: Parser Word64
          version <- skipWhile (== ' ') *> try (Just <$> decimal) <|> return (Nothing) <* skipWhile (== ' ')
          if onlyHeader
            then do
              return (Value key flags len "" version)
            else do
              value <- endline *> take (fromIntegral len) -- XXX
              return (Value key flags len value version)
        "END"          -> pure End
        "STORED"       -> pure Stored
        "NOT_STORED"   -> pure NotStored
        "EXISTS"       -> pure Exists
        "NOT_FOUND"    -> pure NotFound
        "DELETED"      -> pure Deleted
        "OK"           -> pure Ok
        "FOUND"        -> pure Found
        "TOUCHED"      -> pure Touched
        "ERROR"        -> pure Error
        "SERVER_ERROR" -> ServerError <$> (skipWhile (== ' ') *> fmap BS.unpack (AL.takeTill isEndOfLine))
        "CLIENT_ERROR" -> ClientError <$> (skipWhile (== ' ') *> fmap BS.unpack (AL.takeTill isEndOfLine))
        "STAT"         -> Stat
                          <$> (skipWhile (== ' ') *> takeWhile1 (/= ' '))
                          <*> (skipWhile (== ' ') *> AL.takeTill isEndOfLine)
        "VERSION"      -> Version <$> (skipWhile (== ' ') *> AL.takeTill isEndOfLine)
        _              -> fail $ "unknown response " ++ BS.unpack cmd
      _ <- endline
      return (resp)

    word = AB.takeWhile1 (\c -> c /= 32 && c /= 10 && c /= 13)

    ws = AB.skipWhile (== 32)

    codeParser :: Parser Response
    codeParser = Code <$> (skipWhile (== ' ') *> decimal <* skipWhile (== ' ') <* endline)
    
    endline :: Parser BS.ByteString
    endline = try (string "\r\n") <|> string "\n" <|> string "\r"

{-|
  Parse a response.
-}
parseResponse :: BS.ByteString -> Maybe Response
parseResponse = parseResponse' False

{-|
  Parse a response but only its header.
-}
parseResponseHeader :: BS.ByteString -> Maybe Response
parseResponseHeader = parseResponse' True

-- parse one response
parseResponse' :: Bool -> BS.ByteString -> Maybe Response
parseResponse' onlyHeader input = let r = parse (responseParser' onlyHeader) input in case r of
  Fail {} -> Nothing
  Partial parse' -> let r' = parse' "\r\n" in case r' of
    Done _ result -> Just result
    Fail {} -> Nothing
    Partial {} -> Nothing
  Done _ result -> Just result

{-|
  Convert a response to bytestring chunks.
-}
toChunks :: Response -> [BS.ByteString]
toChunks result = case result of
  Ok        -> ["OK\r\n"]
  Value key flag len value version ->
    let vh = maybe [] (\v -> [sp, show' v]) version
        sp = " "
        header = BS.concat $ ["VALUE", sp, key, sp, show' flag, sp, show' len] ++ vh ++ ["\r\n"]
    in [header, value, "\r\n"]
  End       -> ["END\r\n"]
  Stored    -> ["STORED\r\n"]
  NotStored -> ["NOT_STORED\r\n"]
  Exists    -> ["EXISTS\r\n"]
  NotFound  -> ["NOT_FOUND\r\n"]
  Deleted   -> ["DELETED\r\n"]
  Found     -> ["FOUND\r\n"]
  Touched   -> ["TOUCHED\r\n"]
  Error     -> ["ERROR\r\n"]
  ServerError msg -> [BS.concat ["SERVER_ERROR", BS.pack $ if null msg then "" else " " ++ msg, "\r\n"]]
  ClientError msg -> [BS.concat ["CLIENT_ERROR", BS.pack $ if null msg then "" else " " ++ msg, "\r\n"]]
  Version version -> [BS.concat $ [BS.intercalate " " ["VERSION", version], "\r\n"]]
  Code code -> [BS.pack $ show code ++ "\r\n"]
  Stat name value -> [BS.concat ["STAT", " ", name, " ", value, "\r\n"]]
  where
    show' :: (Show a) => a -> BS.ByteString
    show' = BS.pack . show