#
#
#            Nim's Runtime Library
#        (c) Copyright 2012 Dominik Picheta
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#

## This module implements a redis client. It allows you to connect to a
## redis-server instance, send commands and receive replies.
##
## **Beware**: Most (if not all) functions that return a ``TRedisString`` may
## return ``redisNil``, and functions which return a ``TRedisList`` 
## may return ``nil``.

import sockets, os, strutils, parseutils

const
  redisNil* = "\0\0"

type 
  Pipeline = ref object
    enabled: bool
    buffer: string
    expected: int ## number of replies expected if pipelined

type
  SendMode = enum
    normal, pipelined, multiple

type
  Redis* = object
    socket: Socket
    connected: bool
    pipeline: Pipeline
  
  RedisStatus* = string
  RedisInteger* = BiggestInt
  RedisString* = string ## Bulk reply
  RedisList* = seq[RedisString] ## Multi-bulk reply

  ReplyError* = object of IOError ## Invalid reply from redis
  RedisError* = object of IOError        ## Error in redis

{.deprecated: [TSendMode: SendMode, TRedis: Redis, TRedisStatus: RedisStatus,
     TRedisInteger: RedisInteger, TRedisString: RedisString,
     TRedisList: RedisList, EInvalidReply: ReplyError, ERedis: RedisError].}

proc newPipeline(): Pipeline =
  new(result)
  result.buffer = ""
  result.enabled = false
  result.expected = 0

proc open*(host = "localhost", port = 6379.Port): Redis =
  ## Opens a connection to the redis server.
  result.socket = socket(buffered = false)
  if result.socket == invalidSocket:
    raiseOSError(osLastError())
  result.socket.connect(host, port)
  result.pipeline = newPipeline()  

proc raiseInvalidReply(expected, got: char) =
  raise newException(ReplyError, 
          "Expected '$1' at the beginning of a status reply got '$2'" %
          [$expected, $got])

proc raiseNoOK(status: string, pipelineEnabled: bool) =
  if pipelineEnabled and not (status == "QUEUED" or status == "PIPELINED"):
    raise newException(ReplyError, "Expected \"QUEUED\" or \"PIPELINED\" got \"$1\"" % status)
  elif not pipelineEnabled and status != "OK":
    raise newException(ReplyError, "Expected \"OK\" got \"$1\"" % status)

template readSocket(r: Redis, dummyVal:expr): stmt =
  var line {.inject.}: TaintedString = ""
  if r.pipeline.enabled:
    return dummyVal
  else:
    readLine(r.socket, line)

proc parseStatus(r: Redis, line: string = ""): RedisStatus =
  if r.pipeline.enabled:
    return "PIPELINED"

  if line == "":
    raise newException(RedisError, "Server closed connection prematurely")

  if line[0] == '-':
    raise newException(RedisError, strip(line))
  if line[0] != '+':
    raiseInvalidReply('+', line[0])
  
  return line.substr(1) # Strip '+'

proc readStatus(r:Redis): RedisStatus =
  r.readSocket("PIPELINED")
  return r.parseStatus(line)
 
proc parseInteger(r: Redis, line: string = ""): RedisInteger =
  if r.pipeline.enabled: return -1
  
  #if line == "+QUEUED":  # inside of multi
  #  return -1

  if line == "":
    raise newException(RedisError, "Server closed connection prematurely")

  if line[0] == '-':
    raise newException(RedisError, strip(line))
  if line[0] != ':':
    raiseInvalidReply(':', line[0])
  
  # Strip ':'
  if parseBiggestInt(line, result, 1) == 0:
    raise newException(ReplyError, "Unable to parse integer.") 

proc readInteger(r: Redis): RedisInteger =
  r.readSocket(-1)
  return r.parseInteger(line)

proc recv(sock: Socket, size: int): TaintedString =
  result = newString(size).TaintedString
  if sock.recv(cstring(result), size) != size:
    raise newException(ReplyError, "recv failed")

proc parseSingleString(r: Redis, line:string, allowMBNil = false): RedisString =
  if r.pipeline.enabled: return ""
  
  # Error.
  if line[0] == '-':
    raise newException(RedisError, strip(line))
  
  # Some commands return a /bulk/ value or a /multi-bulk/ nil. Odd.
  if allowMBNil:
    if line == "*-1":
       return redisNil
  
  if line[0] != '$':
    raiseInvalidReply('$', line[0])
  
  var numBytes = parseInt(line.substr(1))
  if numBytes == -1:
    return redisNil

  var s = r.socket.recv(numBytes+2)
  result = strip(s.string)

proc readSingleString(r: Redis): RedisString =
  r.readSocket("")
  return r.parseSingleString(line)

proc readNext(r: Redis): RedisList

proc parseArrayLines(r: Redis, countLine:string): RedisList =
  if countLine.string[0] != '*':
    raiseInvalidReply('*', countLine.string[0])

  var numElems = parseInt(countLine.string.substr(1))
  if numElems == -1: return nil
  result = @[]

  for i in 1..numElems:
    var parsed = r.readNext()
    if not isNil(parsed):
      for item in parsed:
        result.add(item)

proc readArrayLines(r: Redis): RedisList =
  r.readSocket(nil)
  return r.parseArrayLines(line)  

proc parseBulkString(r: Redis, allowMBNil = false, line:string = ""): RedisString =
  if r.pipeline.enabled: return ""

  return r.parseSingleString(line, allowMBNil)

proc readBulkString(r: Redis, allowMBNil = false): RedisString =
  r.readSocket("")
  return r.parseBulkString(allowMBNil, line)

proc readArray(r: Redis): RedisList =
  r.readSocket(@[])
  return r.parseArrayLines(line)

proc readNext(r: Redis): RedisList =
  r.readSocket(@[])

  var res = case line[0]
    of '+', '-': @[r.parseStatus(line)]
    of ':': @[$(r.parseInteger(line))]
    of '$': @[r.parseBulkString(true,line)]
    of '*': r.parseArrayLines(line)
    else: 
      raise newException(ReplyError, "readNext failed on line: " & line)
      nil
  r.pipeline.expected -= 1
  return res

proc flushPipeline*(r: Redis, wasMulti = false): RedisList =
  ## Send buffered commands, clear buffer, return results
  if r.pipeline.buffer.len > 0:
    r.socket.send(r.pipeline.buffer)
  r.pipeline.buffer = ""
  
  r.pipeline.enabled = false
  result = @[]
  
  var tot = r.pipeline.expected

  for i in 0..tot-1:
    var ret = r.readNext()
    for item in ret:
     if not (item.contains("OK") or item.contains("QUEUED")):
       result.add(item)

  r.pipeline.expected = 0

proc startPipelining*(r: Redis) =
  ## Enable command pipelining (reduces network roundtrips).
  ## Note that when enabled, you must call flushPipeline to actually send commands, except
  ## for multi/exec() which enable and flush the pipeline automatically.
  ## Commands return immediately with dummy values; actual results returned from
  ## flushPipeline() or exec()
  r.pipeline.expected = 0
  r.pipeline.enabled = true

proc sendCommand(r: Redis, cmd: string, args: varargs[string]) =
  var request = "*" & $(1 + args.len()) & "\c\L"
  request.add("$" & $cmd.len() & "\c\L")
  request.add(cmd & "\c\L")
  for i in items(args):
    request.add("$" & $i.len() & "\c\L")
    request.add(i & "\c\L")
  
  if r.pipeline.enabled:
    r.pipeline.buffer.add(request)
    r.pipeline.expected += 1
  else:
    r.socket.send(request)

proc sendCommand(r: Redis, cmd: string, arg1: string,
                 args: varargs[string]) =
  var request = "*" & $(2 + args.len()) & "\c\L"
  request.add("$" & $cmd.len() & "\c\L")
  request.add(cmd & "\c\L")
  request.add("$" & $arg1.len() & "\c\L")
  request.add(arg1 & "\c\L")
  for i in items(args):
    request.add("$" & $i.len() & "\c\L")
    request.add(i & "\c\L")
    
  if r.pipeline.enabled:
    r.pipeline.expected += 1
    r.pipeline.buffer.add(request)
  else:
    r.socket.send(request)

# Keys

proc del*(r: Redis, keys: varargs[string]): RedisInteger =
  ## Delete a key or multiple keys
  r.sendCommand("DEL", keys)
  return r.readInteger()

proc exists*(r: Redis, key: string): bool =
  ## Determine if a key exists
  r.sendCommand("EXISTS", key)
  return r.readInteger() == 1

proc expire*(r: Redis, key: string, seconds: int): bool =
  ## Set a key's time to live in seconds. Returns `false` if the key could
  ## not be found or the timeout could not be set.
  r.sendCommand("EXPIRE", key, $seconds)
  return r.readInteger() == 1

proc expireAt*(r: Redis, key: string, timestamp: int): bool =
  ## Set the expiration for a key as a UNIX timestamp. Returns `false` 
  ## if the key could not be found or the timeout could not be set.
  r.sendCommand("EXPIREAT", key, $timestamp)
  return r.readInteger() == 1

proc keys*(r: Redis, pattern: string): RedisList =
  ## Find all keys matching the given pattern
  r.sendCommand("KEYS", pattern)
  return r.readArray()

proc scan*(r: Redis, cursor: var BiggestInt): RedisList =
  ## Find all keys matching the given pattern and yield it to client in portions
  ## using default Redis values for MATCH and COUNT parameters
  r.sendCommand("SCAN", $cursor)
  let reply = r.readArray()
  cursor = strutils.parseBiggestInt(reply[0])
  return reply[1..high(reply)]

proc scan*(r: Redis, cursor: var BiggestInt, pattern: string): RedisList =
  ## Find all keys matching the given pattern and yield it to client in portions
  ## using cursor as a client query identifier. Using default Redis value for COUNT argument
  r.sendCommand("SCAN", $cursor, ["MATCH", pattern])
  let reply = r.readArray()
  cursor = strutils.parseBiggestInt(reply[0])
  return reply[1..high(reply)]

proc scan*(r: Redis, cursor: var BiggestInt, pattern: string, count: int): RedisList = 
  ## Find all keys matching the given pattern and yield it to client in portions
  ## using cursor as a client query identifier.
  r.sendCommand("SCAN", $cursor, ["MATCH", pattern, "COUNT", $count])
  let reply = r.readArray()
  cursor = strutils.parseBiggestInt(reply[0])
  return reply[1..high(reply)]

proc move*(r: Redis, key: string, db: int): bool =
  ## Move a key to another database. Returns `true` on a successful move.
  r.sendCommand("MOVE", key, $db)
  return r.readInteger() == 1

proc persist*(r: Redis, key: string): bool =
  ## Remove the expiration from a key. 
  ## Returns `true` when the timeout was removed.
  r.sendCommand("PERSIST", key)
  return r.readInteger() == 1
  
proc randomKey*(r: Redis): RedisString =
  ## Return a random key from the keyspace
  r.sendCommand("RANDOMKEY")
  return r.readBulkString()

proc rename*(r: Redis, key, newkey: string): RedisStatus =
  ## Rename a key.
  ## 
  ## **WARNING:** Overwrites `newkey` if it exists!
  r.sendCommand("RENAME", key, newkey)
  raiseNoOK(r.readStatus(), r.pipeline.enabled)
  
proc renameNX*(r: Redis, key, newkey: string): bool =
  ## Same as ``rename`` but doesn't continue if `newkey` exists.
  ## Returns `true` if key was renamed.
  r.sendCommand("RENAMENX", key, newkey)
  return r.readInteger() == 1

proc ttl*(r: Redis, key: string): RedisInteger =
  ## Get the time to live for a key
  r.sendCommand("TTL", key)
  return r.readInteger()
  
proc keyType*(r: Redis, key: string): RedisStatus =
  ## Determine the type stored at key
  r.sendCommand("TYPE", key)
  return r.readStatus()
  

# Strings

proc append*(r: Redis, key, value: string): RedisInteger =
  ## Append a value to a key
  r.sendCommand("APPEND", key, value)
  return r.readInteger()

proc decr*(r: Redis, key: string): RedisInteger =
  ## Decrement the integer value of a key by one
  r.sendCommand("DECR", key)
  return r.readInteger()
  
proc decrBy*(r: Redis, key: string, decrement: int): RedisInteger =
  ## Decrement the integer value of a key by the given number
  r.sendCommand("DECRBY", key, $decrement)
  return r.readInteger()
  
proc get*(r: Redis, key: string): RedisString =
  ## Get the value of a key. Returns `redisNil` when `key` doesn't exist.
  r.sendCommand("GET", key)
  return r.readBulkString()

proc getBit*(r: Redis, key: string, offset: int): RedisInteger =
  ## Returns the bit value at offset in the string value stored at key
  r.sendCommand("GETBIT", key, $offset)
  return r.readInteger()

proc getRange*(r: Redis, key: string, start, stop: int): RedisString =
  ## Get a substring of the string stored at a key
  r.sendCommand("GETRANGE", key, $start, $stop)
  return r.readBulkString()

proc getSet*(r: Redis, key: string, value: string): RedisString =
  ## Set the string value of a key and return its old value. Returns `redisNil`
  ## when key doesn't exist.
  r.sendCommand("GETSET", key, value)
  return r.readBulkString()

proc incr*(r: Redis, key: string): RedisInteger =
  ## Increment the integer value of a key by one.
  r.sendCommand("INCR", key)
  return r.readInteger()

proc incrBy*(r: Redis, key: string, increment: int): RedisInteger =
  ## Increment the integer value of a key by the given number
  r.sendCommand("INCRBY", key, $increment)
  return r.readInteger()

proc setk*(r: Redis, key, value: string) = 
  ## Set the string value of a key.
  ##
  ## NOTE: This function had to be renamed due to a clash with the `set` type.
  r.sendCommand("SET", key, value)
  raiseNoOK(r.readStatus(), r.pipeline.enabled)

proc setNX*(r: Redis, key, value: string): bool =
  ## Set the value of a key, only if the key does not exist. Returns `true`
  ## if the key was set.
  r.sendCommand("SETNX", key, value)
  return r.readInteger() == 1

proc setBit*(r: Redis, key: string, offset: int, 
             value: string): RedisInteger =
  ## Sets or clears the bit at offset in the string value stored at key
  r.sendCommand("SETBIT", key, $offset, value)
  return r.readInteger()
  
proc setEx*(r: Redis, key: string, seconds: int, value: string): RedisStatus =
  ## Set the value and expiration of a key
  r.sendCommand("SETEX", key, $seconds, value)
  raiseNoOK(r.readStatus(), r.pipeline.enabled)

proc setRange*(r: Redis, key: string, offset: int, 
               value: string): RedisInteger =
  ## Overwrite part of a string at key starting at the specified offset
  r.sendCommand("SETRANGE", key, $offset, value)
  return r.readInteger()

proc strlen*(r: Redis, key: string): RedisInteger =
  ## Get the length of the value stored in a key. Returns 0 when key doesn't
  ## exist.
  r.sendCommand("STRLEN", key)
  return r.readInteger()

# Hashes
proc hDel*(r: Redis, key, field: string): bool =
  ## Delete a hash field at `key`. Returns `true` if the field was removed.
  r.sendCommand("HDEL", key, field)
  return r.readInteger() == 1

proc hExists*(r: Redis, key, field: string): bool =
  ## Determine if a hash field exists.
  r.sendCommand("HEXISTS", key, field)
  return r.readInteger() == 1

proc hGet*(r: Redis, key, field: string): RedisString =
  ## Get the value of a hash field
  r.sendCommand("HGET", key, field)
  return r.readBulkString()

proc hGetAll*(r: Redis, key: string): RedisList =
  ## Get all the fields and values in a hash
  r.sendCommand("HGETALL", key)
  return r.readArray()

proc hIncrBy*(r: Redis, key, field: string, incr: int): RedisInteger =
  ## Increment the integer value of a hash field by the given number
  r.sendCommand("HINCRBY", key, field, $incr)
  return r.readInteger()

proc hKeys*(r: Redis, key: string): RedisList =
  ## Get all the fields in a hash
  r.sendCommand("HKEYS", key)
  return r.readArray()

proc hLen*(r: Redis, key: string): RedisInteger =
  ## Get the number of fields in a hash
  r.sendCommand("HLEN", key)
  return r.readInteger()

proc hMGet*(r: Redis, key: string, fields: varargs[string]): RedisList =
  ## Get the values of all the given hash fields
  r.sendCommand("HMGET", key, fields)
  return r.readArray()

proc hMSet*(r: Redis, key: string, 
            fieldValues: openArray[tuple[field, value: string]]) =
  ## Set multiple hash fields to multiple values
  var args = @[key]
  for field, value in items(fieldValues):
    args.add(field)
    args.add(value)
  r.sendCommand("HMSET", args)
  raiseNoOK(r.readStatus(), r.pipeline.enabled)

proc hSet*(r: Redis, key, field, value: string): RedisInteger =
  ## Set the string value of a hash field
  r.sendCommand("HSET", key, field, value)
  return r.readInteger()
  
proc hSetNX*(r: Redis, key, field, value: string): RedisInteger =
  ## Set the value of a hash field, only if the field does **not** exist
  r.sendCommand("HSETNX", key, field, value)
  return r.readInteger()

proc hVals*(r: Redis, key: string): RedisList =
  ## Get all the values in a hash
  r.sendCommand("HVALS", key)
  return r.readArray()
  
# Lists

proc bLPop*(r: Redis, keys: varargs[string], timeout: int): RedisList =
  ## Remove and get the *first* element in a list, or block until 
  ## one is available
  var args: seq[string] = @[]
  for i in items(keys): args.add(i)
  args.add($timeout)
  r.sendCommand("BLPOP", args)
  return r.readArray()

proc bRPop*(r: Redis, keys: varargs[string], timeout: int): RedisList =
  ## Remove and get the *last* element in a list, or block until one 
  ## is available.
  var args: seq[string] = @[]
  for i in items(keys): args.add(i)
  args.add($timeout)
  r.sendCommand("BRPOP", args)
  return r.readArray()

proc bRPopLPush*(r: Redis, source, destination: string,
                 timeout: int): RedisString =
  ## Pop a value from a list, push it to another list and return it; or
  ## block until one is available.
  ##
  ## http://redis.io/commands/brpoplpush
  r.sendCommand("BRPOPLPUSH", source, destination, $timeout)
  return r.readBulkString(true) # Multi-Bulk nil allowed.

proc lIndex*(r: Redis, key: string, index: int): RedisString =
  ## Get an element from a list by its index
  r.sendCommand("LINDEX", key, $index)
  return r.readBulkString()

proc lInsert*(r: Redis, key: string, before: bool, pivot, value: string):
              RedisInteger =
  ## Insert an element before or after another element in a list
  var pos = if before: "BEFORE" else: "AFTER"
  r.sendCommand("LINSERT", key, pos, pivot, value)
  return r.readInteger()
  
proc lLen*(r: Redis, key: string): RedisInteger =
  ## Get the length of a list
  r.sendCommand("LLEN", key)
  return r.readInteger()

proc lPop*(r: Redis, key: string): RedisString =
  ## Remove and get the first element in a list
  r.sendCommand("LPOP", key)
  return r.readBulkString()

proc lPush*(r: Redis, key, value: string, create: bool = true): RedisInteger =
  ## Prepend a value to a list. Returns the length of the list after the push.
  ## The ``create`` param specifies whether a list should be created if it
  ## doesn't exist at ``key``. More specifically if ``create`` is true, `LPUSH` 
  ## will be used, otherwise `LPUSHX`.
  if create:
    r.sendCommand("LPUSH", key, value)
  else:
    r.sendCommand("LPUSHX", key, value)
  return r.readInteger()

proc lRange*(r: Redis, key: string, start, stop: int): RedisList =
  ## Get a range of elements from a list. Returns `nil` when `key` 
  ## doesn't exist.
  r.sendCommand("LRANGE", key, $start, $stop)
  return r.readArray()

proc lRem*(r: Redis, key: string, value: string, count: int = 0): RedisInteger =
  ## Remove elements from a list. Returns the number of elements that have been
  ## removed.
  r.sendCommand("LREM", key, $count, value)
  return r.readInteger()

proc lSet*(r: Redis, key: string, index: int, value: string) =
  ## Set the value of an element in a list by its index
  r.sendCommand("LSET", key, $index, value)
  raiseNoOK(r.readStatus(), r.pipeline.enabled)

proc lTrim*(r: Redis, key: string, start, stop: int)  =
  ## Trim a list to the specified range
  r.sendCommand("LTRIM", key, $start, $stop)
  raiseNoOK(r.readStatus(), r.pipeline.enabled)

proc rPop*(r: Redis, key: string): RedisString =
  ## Remove and get the last element in a list
  r.sendCommand("RPOP", key)
  return r.readBulkString()
  
proc rPopLPush*(r: Redis, source, destination: string): RedisString =
  ## Remove the last element in a list, append it to another list and return it
  r.sendCommand("RPOPLPUSH", source, destination)
  return r.readBulkString()
  
proc rPush*(r: Redis, key, value: string, create: bool = true): RedisInteger =
  ## Append a value to a list. Returns the length of the list after the push.
  ## The ``create`` param specifies whether a list should be created if it
  ## doesn't exist at ``key``. More specifically if ``create`` is true, `RPUSH` 
  ## will be used, otherwise `RPUSHX`.
  if create:
    r.sendCommand("RPUSH", key, value)
  else:
    r.sendCommand("RPUSHX", key, value)
  return r.readInteger()

# Sets

proc sadd*(r: Redis, key: string, member: string): RedisInteger =
  ## Add a member to a set
  r.sendCommand("SADD", key, member)
  return r.readInteger()

proc scard*(r: Redis, key: string): RedisInteger =
  ## Get the number of members in a set
  r.sendCommand("SCARD", key)
  return r.readInteger()

proc sdiff*(r: Redis, keys: varargs[string]): RedisList =
  ## Subtract multiple sets
  r.sendCommand("SDIFF", keys)
  return r.readArray()

proc sdiffstore*(r: Redis, destination: string,
                keys: varargs[string]): RedisInteger =
  ## Subtract multiple sets and store the resulting set in a key
  r.sendCommand("SDIFFSTORE", destination, keys)
  return r.readInteger()

proc sinter*(r: Redis, keys: varargs[string]): RedisList =
  ## Intersect multiple sets
  r.sendCommand("SINTER", keys)
  return r.readArray()

proc sinterstore*(r: Redis, destination: string,
                 keys: varargs[string]): RedisInteger =
  ## Intersect multiple sets and store the resulting set in a key
  r.sendCommand("SINTERSTORE", destination, keys)
  return r.readInteger()

proc sismember*(r: Redis, key: string, member: string): RedisInteger =
  ## Determine if a given value is a member of a set
  r.sendCommand("SISMEMBER", key, member)
  return r.readInteger()

proc smembers*(r: Redis, key: string): RedisList =
  ## Get all the members in a set
  r.sendCommand("SMEMBERS", key)
  return r.readArray()

proc smove*(r: Redis, source: string, destination: string,
           member: string): RedisInteger =
  ## Move a member from one set to another
  r.sendCommand("SMOVE", source, destination, member)
  return r.readInteger()

proc spop*(r: Redis, key: string): RedisString =
  ## Remove and return a random member from a set
  r.sendCommand("SPOP", key)
  return r.readBulkString()

proc srandmember*(r: Redis, key: string): RedisString =
  ## Get a random member from a set
  r.sendCommand("SRANDMEMBER", key)
  return r.readBulkString()

proc srem*(r: Redis, key: string, member: string): RedisInteger =
  ## Remove a member from a set
  r.sendCommand("SREM", key, member)
  return r.readInteger()

proc sunion*(r: Redis, keys: varargs[string]): RedisList =
  ## Add multiple sets
  r.sendCommand("SUNION", keys)
  return r.readArray()

proc sunionstore*(r: Redis, destination: string,
                 key: varargs[string]): RedisInteger =
  ## Add multiple sets and store the resulting set in a key 
  r.sendCommand("SUNIONSTORE", destination, key)
  return r.readInteger()

# Sorted sets

proc zadd*(r: Redis, key: string, score: int, member: string): RedisInteger =
  ## Add a member to a sorted set, or update its score if it already exists
  r.sendCommand("ZADD", key, $score, member)
  return r.readInteger()

proc zcard*(r: Redis, key: string): RedisInteger =
  ## Get the number of members in a sorted set
  r.sendCommand("ZCARD", key)
  return r.readInteger()

proc zcount*(r: Redis, key: string, min: string, max: string): RedisInteger =
  ## Count the members in a sorted set with scores within the given values
  r.sendCommand("ZCOUNT", key, min, max)
  return r.readInteger()

proc zincrby*(r: Redis, key: string, increment: string,
             member: string): RedisString =
  ## Increment the score of a member in a sorted set
  r.sendCommand("ZINCRBY", key, increment, member)
  return r.readBulkString()

proc zinterstore*(r: Redis, destination: string, numkeys: string,
                 keys: openArray[string], weights: openArray[string] = [],
                 aggregate: string = ""): RedisInteger =
  ## Intersect multiple sorted sets and store the resulting sorted set in
  ## a new key
  var args = @[destination, numkeys]
  for i in items(keys): args.add(i)
  
  if weights.len != 0:
    args.add("WITHSCORE")
    for i in items(weights): args.add(i)
  if aggregate.len != 0:
    args.add("AGGREGATE")
    args.add(aggregate)
    
  r.sendCommand("ZINTERSTORE", args)
  
  return r.readInteger()

proc zrange*(r: Redis, key: string, start: string, stop: string,
            withScores: bool): RedisList =
  ## Return a range of members in a sorted set, by index
  if not withScores:
    r.sendCommand("ZRANGE", key, start, stop)
  else:
    r.sendCommand("ZRANGE", "WITHSCORES", key, start, stop)
  return r.readArray()

proc zrangebyscore*(r: Redis, key: string, min: string, max: string, 
                   withScore: bool = false, limit: bool = false,
                   limitOffset: int = 0, limitCount: int = 0): RedisList =
  ## Return a range of members in a sorted set, by score
  var args = @[key, min, max]
  
  if withScore: args.add("WITHSCORE")
  if limit: 
    args.add("LIMIT")
    args.add($limitOffset)
    args.add($limitCount)
    
  r.sendCommand("ZRANGEBYSCORE", args)
  return r.readArray()

proc zrank*(r: Redis, key: string, member: string): RedisString =
  ## Determine the index of a member in a sorted set
  r.sendCommand("ZRANK", key, member)
  return r.readBulkString()

proc zrem*(r: Redis, key: string, member: string): RedisInteger =
  ## Remove a member from a sorted set
  r.sendCommand("ZREM", key, member)
  return r.readInteger()

proc zremrangebyrank*(r: Redis, key: string, start: string,
                     stop: string): RedisInteger =
  ## Remove all members in a sorted set within the given indexes
  r.sendCommand("ZREMRANGEBYRANK", key, start, stop)
  return r.readInteger()

proc zremrangebyscore*(r: Redis, key: string, min: string,
                      max: string): RedisInteger =
  ## Remove all members in a sorted set within the given scores
  r.sendCommand("ZREMRANGEBYSCORE", key, min, max)
  return r.readInteger()

proc zrevrange*(r: Redis, key: string, start: string, stop: string,
               withScore: bool): RedisList =
  ## Return a range of members in a sorted set, by index, 
  ## with scores ordered from high to low
  if withScore:
    r.sendCommand("ZREVRANGE", "WITHSCORE", key, start, stop)
  else: r.sendCommand("ZREVRANGE", key, start, stop)
  return r.readArray()

proc zrevrangebyscore*(r: Redis, key: string, min: string, max: string, 
                   withScore: bool = false, limit: bool = false,
                   limitOffset: int = 0, limitCount: int = 0): RedisList =
  ## Return a range of members in a sorted set, by score, with
  ## scores ordered from high to low
  var args = @[key, min, max]
  
  if withScore: args.add("WITHSCORE")
  if limit: 
    args.add("LIMIT")
    args.add($limitOffset)
    args.add($limitCount)
  
  r.sendCommand("ZREVRANGEBYSCORE", args)
  return r.readArray()

proc zrevrank*(r: Redis, key: string, member: string): RedisString =
  ## Determine the index of a member in a sorted set, with
  ## scores ordered from high to low
  r.sendCommand("ZREVRANK", key, member)
  return r.readBulkString()

proc zscore*(r: Redis, key: string, member: string): RedisString =
  ## Get the score associated with the given member in a sorted set
  r.sendCommand("ZSCORE", key, member)
  return r.readBulkString()

proc zunionstore*(r: Redis, destination: string, numkeys: string,
                 keys: openArray[string], weights: openArray[string] = [],
                 aggregate: string = ""): RedisInteger =
  ## Add multiple sorted sets and store the resulting sorted set in a new key 
  var args = @[destination, numkeys]
  for i in items(keys): args.add(i)
  
  if weights.len != 0:
    args.add("WEIGHTS")
    for i in items(weights): args.add(i)
  if aggregate.len != 0:
    args.add("AGGREGATE")
    args.add(aggregate)
    
  r.sendCommand("ZUNIONSTORE", args)
  
  return r.readInteger()

# HyperLogLog

proc pfadd*(r: Redis, key: string, elements: varargs[string]): RedisInteger = 
  ## Add variable number of elements into special 'HyperLogLog' set type
  r.sendCommand("PFADD", key, elements)
  return r.readInteger()

proc pfcount*(r: Redis, key: string): RedisInteger =
  ## Count approximate number of elements in 'HyperLogLog'
  r.sendCommand("PFCOUNT", key)
  return r.readInteger()

proc pfmerge*(r: Redis, destination: string, sources: varargs[string]) =
  ## Merge several source HyperLogLog's into one specified by destKey
  r.sendCommand("PFMERGE", destination, sources)
  raiseNoOK(r.readStatus(), r.pipeline.enabled)

# Pub/Sub

# TODO: pub/sub -- I don't think this will work synchronously.
discard """
proc psubscribe*(r: TRedis, pattern: openarray[string]): ???? =
  ## Listen for messages published to channels matching the given patterns
  r.socket.send("PSUBSCRIBE $#\c\L" % pattern)
  return ???

proc publish*(r: TRedis, channel: string, message: string): TRedisInteger =
  ## Post a message to a channel
  r.socket.send("PUBLISH $# $#\c\L" % [channel, message])
  return r.readInteger()

proc punsubscribe*(r: TRedis, [pattern: openarray[string], : string): ???? =
  ## Stop listening for messages posted to channels matching the given patterns
  r.socket.send("PUNSUBSCRIBE $# $#\c\L" % [[pattern.join(), ])
  return ???

proc subscribe*(r: TRedis, channel: openarray[string]): ???? =
  ## Listen for messages published to the given channels
  r.socket.send("SUBSCRIBE $#\c\L" % channel.join)
  return ???

proc unsubscribe*(r: TRedis, [channel: openarray[string], : string): ???? =
  ## Stop listening for messages posted to the given channels 
  r.socket.send("UNSUBSCRIBE $# $#\c\L" % [[channel.join(), ])
  return ???

"""

# Transactions

proc discardMulti*(r: Redis) =
  ## Discard all commands issued after MULTI
  r.sendCommand("DISCARD")
  raiseNoOK(r.readStatus(), r.pipeline.enabled)

proc exec*(r: Redis): RedisList =
  ## Execute all commands issued after MULTI
  r.sendCommand("EXEC")  
  r.pipeline.enabled = false
  # Will reply with +OK for MULTI/EXEC and +QUEUED for every command
  # between, then with the results
  return r.flushPipeline(true)
  

proc multi*(r: Redis) =
  ## Mark the start of a transaction block
  r.startPipelining()
  r.sendCommand("MULTI")
  raiseNoOK(r.readStatus(), r.pipeline.enabled)

proc unwatch*(r: Redis) =
  ## Forget about all watched keys
  r.sendCommand("UNWATCH")
  raiseNoOK(r.readStatus(), r.pipeline.enabled)

proc watch*(r: Redis, key: varargs[string]) =
  ## Watch the given keys to determine execution of the MULTI/EXEC block 
  r.sendCommand("WATCH", key)
  raiseNoOK(r.readStatus(), r.pipeline.enabled)

# Connection

proc auth*(r: Redis, password: string) =
  ## Authenticate to the server
  r.sendCommand("AUTH", password)
  raiseNoOK(r.readStatus(), r.pipeline.enabled)

proc echoServ*(r: Redis, message: string): RedisString =
  ## Echo the given string
  r.sendCommand("ECHO", message)
  return r.readBulkString()

proc ping*(r: Redis): RedisStatus =
  ## Ping the server
  r.sendCommand("PING")
  return r.readStatus()

proc quit*(r: Redis) =
  ## Close the connection
  r.sendCommand("QUIT")
  raiseNoOK(r.readStatus(), r.pipeline.enabled)

proc select*(r: Redis, index: int): RedisStatus =
  ## Change the selected database for the current connection 
  r.sendCommand("SELECT", $index)
  return r.readStatus()

# Server

proc bgrewriteaof*(r: Redis) =
  ## Asynchronously rewrite the append-only file
  r.sendCommand("BGREWRITEAOF")
  raiseNoOK(r.readStatus(), r.pipeline.enabled)

proc bgsave*(r: Redis) =
  ## Asynchronously save the dataset to disk
  r.sendCommand("BGSAVE")
  raiseNoOK(r.readStatus(), r.pipeline.enabled)

proc configGet*(r: Redis, parameter: string): RedisList =
  ## Get the value of a configuration parameter
  r.sendCommand("CONFIG", "GET", parameter)
  return r.readArray()

proc configSet*(r: Redis, parameter: string, value: string) =
  ## Set a configuration parameter to the given value
  r.sendCommand("CONFIG", "SET", parameter, value)
  raiseNoOK(r.readStatus(), r.pipeline.enabled)

proc configResetStat*(r: Redis) =
  ## Reset the stats returned by INFO
  r.sendCommand("CONFIG", "RESETSTAT")
  raiseNoOK(r.readStatus(), r.pipeline.enabled)

proc dbsize*(r: Redis): RedisInteger =
  ## Return the number of keys in the selected database
  r.sendCommand("DBSIZE")
  return r.readInteger()

proc debugObject*(r: Redis, key: string): RedisStatus =
  ## Get debugging information about a key
  r.sendCommand("DEBUG", "OBJECT", key)
  return r.readStatus()

proc debugSegfault*(r: Redis) =
  ## Make the server crash
  r.sendCommand("DEBUG", "SEGFAULT")

proc flushall*(r: Redis): RedisStatus =
  ## Remove all keys from all databases
  r.sendCommand("FLUSHALL")
  raiseNoOK(r.readStatus(), r.pipeline.enabled)

proc flushdb*(r: Redis): RedisStatus =
  ## Remove all keys from the current database
  r.sendCommand("FLUSHDB")
  raiseNoOK(r.readStatus(), r.pipeline.enabled)

proc info*(r: Redis): RedisString =
  ## Get information and statistics about the server
  r.sendCommand("INFO")
  return r.readBulkString()

proc lastsave*(r: Redis): RedisInteger =
  ## Get the UNIX time stamp of the last successful save to disk
  r.sendCommand("LASTSAVE")
  return r.readInteger()

discard """
proc monitor*(r: TRedis) =
  ## Listen for all requests received by the server in real time
  r.socket.send("MONITOR\c\L")
  raiseNoOK(r.readStatus(), r.pipeline.enabled)
"""

proc save*(r: Redis) =
  ## Synchronously save the dataset to disk
  r.sendCommand("SAVE")
  raiseNoOK(r.readStatus(), r.pipeline.enabled)

proc shutdown*(r: Redis) =
  ## Synchronously save the dataset to disk and then shut down the server
  r.sendCommand("SHUTDOWN")
  var s = "".TaintedString
  r.socket.readLine(s)
  if s.string.len != 0: raise newException(RedisError, s.string)

proc slaveof*(r: Redis, host: string, port: string) =
  ## Make the server a slave of another instance, or promote it as master
  r.sendCommand("SLAVEOF", host, port)
  raiseNoOK(r.readStatus(), r.pipeline.enabled)

iterator hPairs*(r: Redis, key: string): tuple[key, value: string] =
  ## Iterator for keys and values in a hash.
  var 
    contents = r.hGetAll(key)
    k = ""
  for i in items(contents):
    if k == "":
      k = i
    else:
      yield (k, i)
      k = ""

proc someTests(r: Redis, how: SendMode):seq[string] =
  var list:seq[string] = @[]

  if how == pipelined:
    r.startPipelining()
  elif how ==  multiple: 
    r.multi()
    
  r.setk("nim:test", "Testing something.")
  r.setk("nim:utf8", "こんにちは")
  r.setk("nim:esc", "\\ths ągt\\")
  r.setk("nim:int", "1")
  list.add(r.get("nim:esc"))
  list.add($(r.incr("nim:int")))
  list.add(r.get("nim:int"))
  list.add(r.get("nim:utf8"))
  list.add($(r.hSet("test1", "name", "A Test")))
  var res = r.hGetAll("test1")
  for r in res:
    list.add(r)
  list.add(r.get("invalid_key"))
  list.add($(r.lPush("mylist","itema")))
  list.add($(r.lPush("mylist","itemb")))
  r.lTrim("mylist",0,1)
  var p = r.lRange("mylist", 0, -1)

  for i in items(p):
    if not isNil(i):
      list.add(i) 

  list.add(r.debugObject("mylist"))

  r.configSet("timeout", "299")
  var g = r.configGet("timeout")
  for i in items(g):
    list.add(i)

  list.add(r.echoServ("BLAH"))

  case how
  of normal:
    return list
  of pipelined:
    return r.flushPipeline()
  of multiple:
    return r.exec()

proc assertListsIdentical(listA, listB: seq[string]) =
  assert(listA.len == listB.len)
  var i = 0
  for item in listA:
    assert(item == listB[i])
    i = i + 1
  
when not defined(testing) and isMainModule:
  when false:
    var r = open()

    # Test with no pipelining
    var listNormal = r.someTests(normal)

    # Test with pipelining enabled
    var listPipelined = r.someTests(pipelined)
    assertListsIdentical(listNormal, listPipelined)

    # Test with multi/exec() (automatic pipelining)
    var listMulti = r.someTests(multiple)
    assertListsIdentical(listNormal, listMulti)
