{-# LANGUAGE OverloadedStrings, RecordWildCards #-}
module Database.Redis.Cluster.Command where

import Data.Char(toLower)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as Char8
import qualified Data.HashMap.Strict as HM
import Database.Redis.Types(RedisResult(decode))
import Database.Redis.Protocol(Reply(..))

data Flag
    = Write
    | ReadOnly
    | DenyOOM
    | Admin
    | PubSub
    | NoScript
    | Random
    | SortForScript
    | Loading
    | Stale
    | SkipMonitor
    | Asking
    | Fast
    | MovableKeys
    | Other BS.ByteString deriving (Show, Eq)


data AritySpec = Required Integer | MinimumRequired Integer deriving (Show)

data LastKeyPositionSpec = LastKeyPosition Integer | UnlimitedKeys Integer deriving (Show)

newtype InfoMap = InfoMap (HM.HashMap String CommandInfo)

-- Represents the result of the COMMAND command, which returns information
-- about the position of keys in a request
data CommandInfo = CommandInfo
    { name :: BS.ByteString
    , arity :: AritySpec
    , flags :: [Flag]
    , firstKeyPosition :: Integer
    , lastKeyPosition :: LastKeyPositionSpec
    , stepCount :: Integer
    } deriving (Show)

instance RedisResult CommandInfo where
    decode (MultiBulk (Just
        [ Bulk (Just commandName)
        , Integer aritySpec
        , MultiBulk (Just replyFlags)
        , Integer firstKeyPos
        , Integer lastKeyPos
        , Integer replyStepCount])) = do
            parsedFlags <- mapM parseFlag replyFlags
            lastKey <- parseLastKeyPos
            return $ CommandInfo
                { name = commandName
                , arity = parseArity aritySpec
                , flags = parsedFlags
                , firstKeyPosition = firstKeyPos
                , lastKeyPosition = lastKey
                , stepCount = replyStepCount
                } where
        parseArity int = case int of
            i | i >= 0 -> Required i
            i -> MinimumRequired $ abs i
        parseFlag :: Reply -> Either Reply Flag
        parseFlag (SingleLine flag) = return $ case flag of
            "write" -> Write
            "readonly" -> ReadOnly
            "denyoom" -> DenyOOM
            "admin" -> Admin
            "pubsub" -> PubSub
            "noscript" -> NoScript
            "random" -> Random
            "sort_for_script" -> SortForScript
            "loading" -> Loading
            "stale" -> Stale
            "skip_monitor" -> SkipMonitor
            "asking" -> Asking
            "fast" -> Fast
            "movablekeys" -> MovableKeys
            other -> Other other
        parseFlag bad = Left bad
        parseLastKeyPos :: Either Reply LastKeyPositionSpec
        parseLastKeyPos = return $ case lastKeyPos of
            i | i < 0 -> UnlimitedKeys (-i - 1)
            i -> LastKeyPosition i
    -- since redis 6.0
    decode (MultiBulk (Just
        [ name@(Bulk (Just _))
        , arity@(Integer _)
        , flags@(MultiBulk (Just _))
        , firstPos@(Integer _)
        , lastPos@(Integer _)
        , step@(Integer _)
        , MultiBulk _  -- ACL categories
        ])) =
        decode (MultiBulk (Just [name, arity, flags, firstPos, lastPos, step]))

    decode e = Left e

newInfoMap :: [CommandInfo] -> InfoMap
newInfoMap = InfoMap . HM.fromList . map (\c -> (Char8.unpack $ name c, c))

keysForRequest :: InfoMap -> [BS.ByteString] -> Maybe [BS.ByteString]
keysForRequest _ ["DEBUG", "OBJECT", key] =
    -- `COMMAND` output for `DEBUG` would let us believe it doesn't have any
    -- keys, but the `DEBUG OBJECT` subcommand does.
    Just [key]
keysForRequest _ ["QUIT"] =
    -- The `QUIT` command is not listed in the `COMMAND` output.
    Just []
keysForRequest (InfoMap infoMap) request@(command:_) = do
    info <- HM.lookup (map toLower $ Char8.unpack command) infoMap
    keysForRequest' info request
keysForRequest _ [] = Nothing

keysForRequest' :: CommandInfo -> [BS.ByteString] -> Maybe [BS.ByteString]
keysForRequest' info request
    | isMovable info =
        parseMovable request
    | stepCount info == 0 =
        Just []
    | otherwise = do
        let possibleKeys = case lastKeyPosition info of
                LastKeyPosition end -> take (fromEnum $ 1 + end - firstKeyPosition info) $ drop (fromEnum $ firstKeyPosition info) request
                UnlimitedKeys end ->
                    drop (fromEnum $ firstKeyPosition info) $
                       take (length request - fromEnum end) request
        return $ takeEvery (fromEnum $ stepCount info) possibleKeys

isMovable :: CommandInfo -> Bool
isMovable CommandInfo{..} = MovableKeys `elem` flags

parseMovable :: [BS.ByteString] -> Maybe [BS.ByteString]
parseMovable ("SORT":key:_) = Just [key]
parseMovable ("EVAL":_:rest) = readNumKeys rest
parseMovable ("EVALSHA":_:rest) = readNumKeys rest
parseMovable ("ZUNIONSTORE":_:rest) = readNumKeys rest
parseMovable ("ZINTERSTORE":_:rest) = readNumKeys rest
parseMovable ("XREAD":rest) = readXreadKeys rest
parseMovable ("XREADGROUP":"GROUP":_:_:rest) = readXreadgroupKeys rest
parseMovable _ = Nothing

readXreadKeys :: [BS.ByteString] -> Maybe [BS.ByteString]
readXreadKeys ("COUNT":_:rest) = readXreadKeys rest
readXreadKeys ("BLOCK":_:rest) = readXreadKeys rest
readXreadKeys ("STREAMS":rest) = Just $ take (length rest `div` 2) rest
readXreadKeys _ = Nothing

readXreadgroupKeys :: [BS.ByteString] -> Maybe [BS.ByteString]
readXreadgroupKeys ("COUNT":_:rest) = readXreadKeys rest
readXreadgroupKeys ("BLOCK":_:rest) = readXreadKeys rest
readXreadgroupKeys ("NOACK":rest) = readXreadKeys rest
readXreadgroupKeys ("STREAMS":rest) = Just $ take (length rest `div` 2) rest
readXreadgroupKeys _ = Nothing

readNumKeys :: [BS.ByteString] -> Maybe [BS.ByteString]
readNumKeys (rawNumKeys:rest) = do
    numKeys <- readMaybe (Char8.unpack rawNumKeys)
    return $ take numKeys rest
readNumKeys _ = Nothing
-- takeEvery 1 [1,2,3,4,5] ->[1,2,3,4,5]
-- takeEvery 2 [1,2,3,4,5] ->[1,3,5]
-- takeEvery 3 [1,2,3,4,5] ->[1,4]
takeEvery :: Int -> [a] -> [a]
takeEvery _ [] = []
takeEvery n (x:xs) = x : takeEvery n (drop (n-1) xs)

readMaybe :: Read a => String -> Maybe a
readMaybe s = case reads s of
                  [(val, "")] -> Just val
                  _           -> Nothing
