<?php // phpcs:disable

/*
 * This file is part of the Predis package.
 *
 * (c) Daniele Alessandri <suppakilla@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Predis\Command;

use InvalidArgumentException;

/**
 * Defines an abstraction representing a Redis command.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface CommandInterface
{
    /**
     * Returns the ID of the Redis command. By convention, command identifiers
     * must always be uppercase.
     *
     * @return string
     */
    public function getId();

    /**
     * Assign the specified slot to the command for clustering distribution.
     *
     * @param int $slot Slot ID.
     */
    public function setSlot($slot);

    /**
     * Returns the assigned slot of the command for clustering distribution.
     *
     * @return int|null
     */
    public function getSlot();

    /**
     * Sets the arguments for the command.
     *
     * @param array $arguments List of arguments.
     */
    public function setArguments(array $arguments);

    /**
     * Sets the raw arguments for the command without processing them.
     *
     * @param array $arguments List of arguments.
     */
    public function setRawArguments(array $arguments);

    /**
     * Gets the arguments of the command.
     *
     * @return array
     */
    public function getArguments();

    /**
     * Gets the argument of the command at the specified index.
     *
     * @param int $index Index of the desired argument.
     *
     * @return mixed|null
     */
    public function getArgument($index);

    /**
     * Parses a raw response and returns a PHP object.
     *
     * @param string $data Binary string containing the whole response.
     *
     * @return mixed
     */
    public function parseResponse($data);
}

/**
 * Base class for Redis commands.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
abstract class Command implements CommandInterface
{
    private $slot;
    private $arguments = array();

    /**
     * Returns a filtered array of the arguments.
     *
     * @param array $arguments List of arguments.
     *
     * @return array
     */
    protected function filterArguments(array $arguments)
    {
        return $arguments;
    }

    /**
     * {@inheritdoc}
     */
    public function setArguments(array $arguments)
    {
        $this->arguments = $this->filterArguments($arguments);
        unset($this->slot);
    }

    /**
     * {@inheritdoc}
     */
    public function setRawArguments(array $arguments)
    {
        $this->arguments = $arguments;
        unset($this->slot);
    }

    /**
     * {@inheritdoc}
     */
    public function getArguments()
    {
        return $this->arguments;
    }

    /**
     * {@inheritdoc}
     */
    public function getArgument($index)
    {
        if (isset($this->arguments[$index])) {
            return $this->arguments[$index];
        }
    }

    /**
     * {@inheritdoc}
     */
    public function setSlot($slot)
    {
        $this->slot = $slot;
    }

    /**
     * {@inheritdoc}
     */
    public function getSlot()
    {
        if (isset($this->slot)) {
            return $this->slot;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        return $data;
    }

    /**
     * Normalizes the arguments array passed to a Redis command.
     *
     * @param array $arguments Arguments for a command.
     *
     * @return array
     */
    public static function normalizeArguments(array $arguments)
    {
        if (count($arguments) === 1 && is_array($arguments[0])) {
            return $arguments[0];
        }

        return $arguments;
    }

    /**
     * Normalizes the arguments array passed to a variadic Redis command.
     *
     * @param array $arguments Arguments for a command.
     *
     * @return array
     */
    public static function normalizeVariadic(array $arguments)
    {
        if (count($arguments) === 2 && is_array($arguments[1])) {
            return array_merge(array($arguments[0]), $arguments[1]);
        }

        return $arguments;
    }
}

/**
 * @link http://redis.io/commands/zrange
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ZSetRange extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'ZRANGE';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        if (count($arguments) === 4) {
            $lastType = gettype($arguments[3]);

            if ($lastType === 'string' && strtoupper($arguments[3]) === 'WITHSCORES') {
                // Used for compatibility with older versions
                $arguments[3] = array('WITHSCORES' => true);
                $lastType = 'array';
            }

            if ($lastType === 'array') {
                $options = $this->prepareOptions(array_pop($arguments));

                return array_merge($arguments, $options);
            }
        }

        return $arguments;
    }

    /**
     * Returns a list of options and modifiers compatible with Redis.
     *
     * @param array $options List of options.
     *
     * @return array
     */
    protected function prepareOptions($options)
    {
        $opts = array_change_key_case($options, CASE_UPPER);
        $finalizedOpts = array();

        if (!empty($opts['WITHSCORES'])) {
            $finalizedOpts[] = 'WITHSCORES';
        }

        return $finalizedOpts;
    }

    /**
     * Checks for the presence of the WITHSCORES modifier.
     *
     * @return bool
     */
    protected function withScores()
    {
        $arguments = $this->getArguments();

        if (count($arguments) < 4) {
            return false;
        }

        return strtoupper($arguments[3]) === 'WITHSCORES';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        if ($this->withScores()) {
            $result = array();

            for ($i = 0; $i < count($data); $i++) {
                $result[$data[$i]] = $data[++$i];
            }

            return $result;
        }

        return $data;
    }
}

/**
 * @link http://redis.io/commands/sinterstore
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class SetIntersectionStore extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SINTERSTORE';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        if (count($arguments) === 2 && is_array($arguments[1])) {
            return array_merge(array($arguments[0]), $arguments[1]);
        }

        return $arguments;
    }
}

/**
 * @link http://redis.io/commands/sinter
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class SetIntersection extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SINTER';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        return self::normalizeArguments($arguments);
    }
}

/**
 * @link http://redis.io/commands/eval
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerEval extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'EVAL';
    }

    /**
     * Calculates the SHA1 hash of the body of the script.
     *
     * @return string SHA1 hash.
     */
    public function getScriptHash()
    {
        return sha1($this->getArgument(0));
    }
}

/**
 * @link http://redis.io/commands/rename
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class KeyRename extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'RENAME';
    }
}

/**
 * @link http://redis.io/commands/setex
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringSetExpire extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SETEX';
    }
}

/**
 * @link http://redis.io/commands/mset
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringSetMultiple extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'MSET';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        if (count($arguments) === 1 && is_array($arguments[0])) {
            $flattenedKVs = array();
            $args = $arguments[0];

            foreach ($args as $k => $v) {
                $flattenedKVs[] = $k;
                $flattenedKVs[] = $v;
            }

            return $flattenedKVs;
        }

        return $arguments;
    }
}

/**
 * @link http://redis.io/commands/expireat
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class KeyExpireAt extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'EXPIREAT';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        return (bool) $data;
    }
}

/**
 * @link http://redis.io/commands/blpop
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ListPopFirstBlocking extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'BLPOP';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        if (count($arguments) === 2 && is_array($arguments[0])) {
            list($arguments, $timeout) = $arguments;
            array_push($arguments, $timeout);
        }

        return $arguments;
    }
}

/**
 * @link http://redis.io/commands/unsubscribe
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class PubSubUnsubscribe extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'UNSUBSCRIBE';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        return self::normalizeArguments($arguments);
    }
}

/**
 * @link http://redis.io/commands/info
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerInfo extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'INFO';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        $info      = array();
        $infoLines = preg_split('/\r?\n/', $data);

        foreach ($infoLines as $row) {
            if (strpos($row, ':') === false) {
                continue;
            }

            list($k, $v) = $this->parseRow($row);
            $info[$k] = $v;
        }

        return $info;
    }

    /**
     * Parses a single row of the response and returns the key-value pair.
     *
     * @param string $row Single row of the response.
     *
     * @return array
     */
    protected function parseRow($row)
    {
        list($k, $v) = explode(':', $row, 2);

        if (preg_match('/^db\d+$/', $k)) {
            $v = $this->parseDatabaseStats($v);
        }

        return array($k, $v);
    }

    /**
     * Extracts the statistics of each logical DB from the string buffer.
     *
     * @param string $str Response buffer.
     *
     * @return array
     */
    protected function parseDatabaseStats($str)
    {
        $db = array();

        foreach (explode(',', $str) as $dbvar) {
            list($dbvk, $dbvv) = explode('=', $dbvar);
            $db[trim($dbvk)] = $dbvv;
        }

        return $db;
    }

    /**
     * Parses the response and extracts the allocation statistics.
     *
     * @param string $str Response buffer.
     *
     * @return array
     */
    protected function parseAllocationStats($str)
    {
        $stats = array();

        foreach (explode(',', $str) as $kv) {
            @list($size, $objects, $extra) = explode('=', $kv);

            // hack to prevent incorrect values when parsing the >=256 key
            if (isset($extra)) {
                $size = ">=$objects";
                $objects = $extra;
            }

            $stats[$size] = $objects;
        }

        return $stats;
    }
}

/**
 * @link http://redis.io/commands/evalsha
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerEvalSHA extends ServerEval
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'EVALSHA';
    }

    /**
     * Returns the SHA1 hash of the body of the script.
     *
     * @return string SHA1 hash.
     */
    public function getScriptHash()
    {
        return $this->getArgument(0);
    }
}

/**
 * @link http://redis.io/commands/expire
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class KeyExpire extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'EXPIRE';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        return (bool) $data;
    }
}

/**
 * @link http://redis.io/commands/subscribe
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class PubSubSubscribe extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SUBSCRIBE';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        return self::normalizeArguments($arguments);
    }
}

/**
 * @link http://redis.io/commands/rpush
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ListPushTail extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'RPUSH';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        return self::normalizeVariadic($arguments);
    }
}

/**
 * @link http://redis.io/commands/ttl
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class KeyTimeToLive extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'TTL';
    }
}

/**
 * @link http://redis.io/commands/zunionstore
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ZSetUnionStore extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'ZUNIONSTORE';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        $options = array();
        $argc = count($arguments);

        if ($argc > 2 && is_array($arguments[$argc - 1])) {
            $options = $this->prepareOptions(array_pop($arguments));
        }

        if (is_array($arguments[1])) {
            $arguments = array_merge(
                array($arguments[0], count($arguments[1])),
                $arguments[1]
            );
        }

        return array_merge($arguments, $options);
    }

    /**
     * Returns a list of options and modifiers compatible with Redis.
     *
     * @param array $options List of options.
     *
     * @return array
     */
    private function prepareOptions($options)
    {
        $opts = array_change_key_case($options, CASE_UPPER);
        $finalizedOpts = array();

        if (isset($opts['WEIGHTS']) && is_array($opts['WEIGHTS'])) {
            $finalizedOpts[] = 'WEIGHTS';

            foreach ($opts['WEIGHTS'] as $weight) {
                $finalizedOpts[] = $weight;
            }
        }

        if (isset($opts['AGGREGATE'])) {
            $finalizedOpts[] = 'AGGREGATE';
            $finalizedOpts[] = $opts['AGGREGATE'];
        }

        return $finalizedOpts;
    }
}

/**
 * @link http://redis.io/commands/zrangebyscore
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ZSetRangeByScore extends ZSetRange
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'ZRANGEBYSCORE';
    }

    /**
     * {@inheritdoc}
     */
    protected function prepareOptions($options)
    {
        $opts = array_change_key_case($options, CASE_UPPER);
        $finalizedOpts = array();

        if (isset($opts['LIMIT']) && is_array($opts['LIMIT'])) {
            $limit = array_change_key_case($opts['LIMIT'], CASE_UPPER);

            $finalizedOpts[] = 'LIMIT';
            $finalizedOpts[] = isset($limit['OFFSET']) ? $limit['OFFSET'] : $limit[0];
            $finalizedOpts[] = isset($limit['COUNT']) ? $limit['COUNT'] : $limit[1];
        }

        return array_merge($finalizedOpts, parent::prepareOptions($options));
    }

    /**
     * {@inheritdoc}
     */
    protected function withScores()
    {
        $arguments = $this->getArguments();

        for ($i = 3; $i < count($arguments); $i++) {
            switch (strtoupper($arguments[$i])) {
                case 'WITHSCORES':
                    return true;

                case 'LIMIT':
                    $i += 2;
                    break;
            }
        }

        return false;
    }
}

/**
 * @link http://redis.io/commands/zremrangebyrank
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ZSetRemoveRangeByRank extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'ZREMRANGEBYRANK';
    }
}

/**
 * @link http://redis.io/commands/spop
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class SetPop extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SPOP';
    }
}

/**
 * @link http://redis.io/commands/smove
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class SetMove extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SMOVE';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        return (bool) $data;
    }
}

/**
 * @link http://redis.io/commands/sismember
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class SetIsMember extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SISMEMBER';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        return (bool) $data;
    }
}

/**
 * @link http://redis.io/commands/smembers
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class SetMembers extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SMEMBERS';
    }
}

/**
 * @link http://redis.io/commands/zremrangebyscore
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ZSetRemoveRangeByScore extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'ZREMRANGEBYSCORE';
    }
}

/**
 * @link http://redis.io/commands/srandmember
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class SetRandomMember extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SRANDMEMBER';
    }
}

/**
 * @link http://redis.io/commands/sscan
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class SetScan extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SSCAN';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        if (count($arguments) === 3 && is_array($arguments[2])) {
            $options = $this->prepareOptions(array_pop($arguments));
            $arguments = array_merge($arguments, $options);
        }

        return $arguments;
    }

    /**
     * Returns a list of options and modifiers compatible with Redis.
     *
     * @param array $options List of options.
     *
     * @return array
     */
    protected function prepareOptions($options)
    {
        $options = array_change_key_case($options, CASE_UPPER);
        $normalized = array();

        if (!empty($options['MATCH'])) {
            $normalized[] = 'MATCH';
            $normalized[] = $options['MATCH'];
        }

        if (!empty($options['COUNT'])) {
            $normalized[] = 'COUNT';
            $normalized[] = $options['COUNT'];
        }

        return $normalized;
    }
}

/**
 * @link http://redis.io/commands/zremrangebylex
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ZSetRemoveRangeByLex extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'ZREMRANGEBYLEX';
    }
}

/**
 * @link http://redis.io/commands/bitop
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringBitOp extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'BITOP';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        if (count($arguments) === 3 && is_array($arguments[2])) {
            list($operation, $destination, ) = $arguments;
            $arguments = $arguments[2];
            array_unshift($arguments, $operation, $destination);
        }

        return $arguments;
    }
}

/**
 * @link http://redis.io/commands/bitcount
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringBitCount extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'BITCOUNT';
    }
}

/**
 * @link http://redis.io/commands/append
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringAppend extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'APPEND';
    }
}

/**
 * @link http://redis.io/commands/sunion
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class SetUnion extends SetIntersection
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SUNION';
    }
}

/**
 * @link http://redis.io/commands/sunionstore
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class SetUnionStore extends SetIntersectionStore
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SUNIONSTORE';
    }
}

/**
 * @link http://redis.io/commands/srem
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class SetRemove extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SREM';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        return self::normalizeVariadic($arguments);
    }
}

/**
 * @link http://redis.io/commands/zrevrange
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ZSetReverseRange extends ZSetRange
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'ZREVRANGE';
    }
}

/**
 * @link http://redis.io/commands/slowlog
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerSlowlog extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SLOWLOG';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        if (is_array($data)) {
            $log = array();

            foreach ($data as $index => $entry) {
                $log[$index] = array(
                    'id' => $entry[0],
                    'timestamp' => $entry[1],
                    'duration' => $entry[2],
                    'command' => $entry[3],
                );
            }

            return $log;
        }

        return $data;
    }
}

/**
 * @link http://redis.io/commands/zscore
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ZSetScore extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'ZSCORE';
    }
}

/**
 * @link http://redis.io/commands/slaveof
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerSlaveOf extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SLAVEOF';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        if (count($arguments) === 0 || $arguments[0] === 'NO ONE') {
            return array('NO', 'ONE');
        }

        return $arguments;
    }
}

/**
 * @link http://redis.io/commands/shutdown
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerShutdown extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SHUTDOWN';
    }
}

/**
 * @link http://redis.io/commands/script
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerScript extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SCRIPT';
    }
}

/**
 * @link http://redis.io/topics/sentinel
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerSentinel extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SENTINEL';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        switch (strtolower($this->getArgument(0))) {
            case 'masters':
            case 'slaves':
                return self::processMastersOrSlaves($data);

            default:
                return $data;
        }
    }

    /**
     * Returns a processed response to SENTINEL MASTERS or SENTINEL SLAVES.
     *
     * @param array $servers List of Redis servers.
     *
     * @return array
     */
    protected static function processMastersOrSlaves(array $servers)
    {
        foreach ($servers as $idx => $node) {
            $processed = array();
            $count = count($node);

            for ($i = 0; $i < $count; $i++) {
                $processed[$node[$i]] = $node[++$i];
            }

            $servers[$idx] = $processed;
        }

        return $servers;
    }
}

/**
 * @link http://redis.io/commands/zscan
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ZSetScan extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'ZSCAN';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        if (count($arguments) === 3 && is_array($arguments[2])) {
            $options = $this->prepareOptions(array_pop($arguments));
            $arguments = array_merge($arguments, $options);
        }

        return $arguments;
    }

    /**
     * Returns a list of options and modifiers compatible with Redis.
     *
     * @param array $options List of options.
     *
     * @return array
     */
    protected function prepareOptions($options)
    {
        $options = array_change_key_case($options, CASE_UPPER);
        $normalized = array();

        if (!empty($options['MATCH'])) {
            $normalized[] = 'MATCH';
            $normalized[] = $options['MATCH'];
        }

        if (!empty($options['COUNT'])) {
            $normalized[] = 'COUNT';
            $normalized[] = $options['COUNT'];
        }

        return $normalized;
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        if (is_array($data)) {
            $members = $data[1];
            $result = array();

            for ($i = 0; $i < count($members); $i++) {
                $result[$members[$i]] = (float) $members[++$i];
            }

            $data[1] = $result;
        }

        return $data;
    }
}

/**
 * @link http://redis.io/commands/zrevrank
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ZSetReverseRank extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'ZREVRANK';
    }
}

/**
 * @link http://redis.io/commands/sdiffstore
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class SetDifferenceStore extends SetIntersectionStore
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SDIFFSTORE';
    }
}

/**
 * @link http://redis.io/commands/zrevrangebyscore
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ZSetReverseRangeByScore extends ZSetRangeByScore
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'ZREVRANGEBYSCORE';
    }
}

/**
 * @link http://redis.io/commands/sdiff
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class SetDifference extends SetIntersection
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SDIFF';
    }
}

/**
 * @link http://redis.io/commands/scard
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class SetCardinality extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SCARD';
    }
}

/**
 * @link http://redis.io/commands/time
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerTime extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'TIME';
    }
}

/**
 * @link http://redis.io/commands/sadd
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class SetAdd extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SADD';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        return self::normalizeVariadic($arguments);
    }
}

/**
 * @link http://redis.io/commands/bitpos
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringBitPos extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'BITPOS';
    }
}

/**
 * @link http://redis.io/commands/decrby
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringDecrementBy extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'DECRBY';
    }
}

/**
 * @link http://redis.io/commands/substr
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringSubstr extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SUBSTR';
    }
}

/**
 * @link http://redis.io/commands/zlexcount
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ZSetLexCount extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'ZLEXCOUNT';
    }
}

/**
 * @link http://redis.io/commands/zinterstore
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ZSetIntersectionStore extends ZSetUnionStore
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'ZINTERSTORE';
    }
}

/**
 * @link http://redis.io/commands/strlen
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringStrlen extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'STRLEN';
    }
}

/**
 * @link http://redis.io/commands/setrange
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringSetRange extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SETRANGE';
    }
}

/**
 * @link http://redis.io/commands/msetnx
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringSetMultiplePreserve extends StringSetMultiple
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'MSETNX';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        return (bool) $data;
    }
}

/**
 * @link http://redis.io/commands/setnx
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringSetPreserve extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SETNX';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        return (bool) $data;
    }
}

/**
 * @link http://redis.io/commands/discard
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class TransactionDiscard extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'DISCARD';
    }
}

/**
 * @link http://redis.io/commands/exec
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class TransactionExec extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'EXEC';
    }
}

/**
 * @link http://redis.io/commands/zcard
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ZSetCardinality extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'ZCARD';
    }
}

/**
 * @link http://redis.io/commands/zcount
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ZSetCount extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'ZCOUNT';
    }
}

/**
 * @link http://redis.io/commands/zadd
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ZSetAdd extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'ZADD';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        if (count($arguments) === 2 && is_array($arguments[1])) {
            $flattened = array($arguments[0]);

            foreach ($arguments[1] as $member => $score) {
                $flattened[] = $score;
                $flattened[] = $member;
            }

            return $flattened;
        }

        return $arguments;
    }
}

/**
 * @link http://redis.io/commands/watch
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class TransactionWatch extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'WATCH';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        if (isset($arguments[0]) && is_array($arguments[0])) {
            return $arguments[0];
        }

        return $arguments;
    }
}

/**
 * @link http://redis.io/commands/multi
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class TransactionMulti extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'MULTI';
    }
}

/**
 * @link http://redis.io/commands/unwatch
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class TransactionUnwatch extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'UNWATCH';
    }
}

/**
 * @link http://redis.io/commands/zrangebylex
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ZSetRangeByLex extends ZSetRange
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'ZRANGEBYLEX';
    }

    /**
     * {@inheritdoc}
     */
    protected function prepareOptions($options)
    {
        $opts = array_change_key_case($options, CASE_UPPER);
        $finalizedOpts = array();

        if (isset($opts['LIMIT']) && is_array($opts['LIMIT'])) {
            $limit = array_change_key_case($opts['LIMIT'], CASE_UPPER);

            $finalizedOpts[] = 'LIMIT';
            $finalizedOpts[] = isset($limit['OFFSET']) ? $limit['OFFSET'] : $limit[0];
            $finalizedOpts[] = isset($limit['COUNT']) ? $limit['COUNT'] : $limit[1];
        }

        return $finalizedOpts;
    }

    /**
     * {@inheritdoc}
     */
    protected function withScores()
    {
        return false;
    }
}

/**
 * @link http://redis.io/commands/zrank
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ZSetRank extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'ZRANK';
    }
}

/**
 * @link http://redis.io/commands/mget
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringGetMultiple extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'MGET';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        return self::normalizeArguments($arguments);
    }
}

/**
 * @link http://redis.io/commands/getrange
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringGetRange extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'GETRANGE';
    }
}

/**
 * @link http://redis.io/commands/zrem
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ZSetRemove extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'ZREM';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        return self::normalizeVariadic($arguments);
    }
}

/**
 * @link http://redis.io/commands/getbit
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringGetBit extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'GETBIT';
    }
}

/**
 * @link http://redis.io/commands/zincrby
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ZSetIncrementBy extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'ZINCRBY';
    }
}

/**
 * @link http://redis.io/commands/get
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringGet extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'GET';
    }
}

/**
 * @link http://redis.io/commands/getset
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringGetSet extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'GETSET';
    }
}

/**
 * @link http://redis.io/commands/incr
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringIncrement extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'INCR';
    }
}

/**
 * @link http://redis.io/commands/set
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringSet extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SET';
    }
}

/**
 * @link http://redis.io/commands/setbit
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringSetBit extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SETBIT';
    }
}

/**
 * @link http://redis.io/commands/psetex
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringPreciseSetExpire extends StringSetExpire
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'PSETEX';
    }
}

/**
 * @link http://redis.io/commands/incrbyfloat
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringIncrementByFloat extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'INCRBYFLOAT';
    }
}

/**
 * @link http://redis.io/commands/incrby
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringIncrementBy extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'INCRBY';
    }
}

/**
 * @link http://redis.io/commands/save
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerSave extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SAVE';
    }
}

/**
 * @link http://redis.io/commands/decr
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StringDecrement extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'DECR';
    }
}

/**
 * @link http://redis.io/commands/flushall
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerFlushAll extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'FLUSHALL';
    }
}

/**
 * @link http://redis.io/commands/del
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class KeyDelete extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'DEL';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        return self::normalizeArguments($arguments);
    }
}

/**
 * @link http://redis.io/commands/dump
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class KeyDump extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'DUMP';
    }
}

/**
 * @link http://redis.io/commands/exists
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class KeyExists extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'EXISTS';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        return (bool) $data;
    }
}

/**
 * @link http://redis.io/commands/pfmerge
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class HyperLogLogMerge extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'PFMERGE';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        return self::normalizeArguments($arguments);
    }
}

/**
 * @link http://redis.io/commands/pfcount
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class HyperLogLogCount extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'PFCOUNT';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        return self::normalizeArguments($arguments);
    }
}

/**
 * @link http://redis.io/commands/hvals
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class HashValues extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'HVALS';
    }
}

/**
 * @link http://redis.io/commands/pfadd
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class HyperLogLogAdd extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'PFADD';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        return self::normalizeVariadic($arguments);
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        return (bool) $data;
    }
}

/**
 * @link http://redis.io/commands/keys
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class KeyKeys extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'KEYS';
    }
}

/**
 * @link http://redis.io/commands/move
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class KeyMove extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'MOVE';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        return (bool) $data;
    }
}

/**
 * @link http://redis.io/commands/randomkey
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class KeyRandom extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'RANDOMKEY';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        return $data !== '' ? $data : null;
    }
}

/**
 * @link http://redis.io/commands/renamenx
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class KeyRenamePreserve extends KeyRename
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'RENAMENX';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        return (bool) $data;
    }
}

/**
 * @link http://redis.io/commands/restore
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class KeyRestore extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'RESTORE';
    }
}

/**
 * @link http://redis.io/commands/pttl
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class KeyPreciseTimeToLive extends KeyTimeToLive
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'PTTL';
    }
}

/**
 * @link http://redis.io/commands/pexpireat
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class KeyPreciseExpireAt extends KeyExpireAt
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'PEXPIREAT';
    }
}

/**
 * @link http://redis.io/commands/persist
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class KeyPersist extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'PERSIST';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        return (bool) $data;
    }
}

/**
 * @link http://redis.io/commands/pexpire
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class KeyPreciseExpire extends KeyExpire
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'PEXPIRE';
    }
}

/**
 * @link http://redis.io/commands/hsetnx
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class HashSetPreserve extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'HSETNX';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        return (bool) $data;
    }
}

/**
 * @link http://redis.io/commands/hmset
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class HashSetMultiple extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'HMSET';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        if (count($arguments) === 2 && is_array($arguments[1])) {
            $flattenedKVs = array($arguments[0]);
            $args = $arguments[1];

            foreach ($args as $k => $v) {
                $flattenedKVs[] = $k;
                $flattenedKVs[] = $v;
            }

            return $flattenedKVs;
        }

        return $arguments;
    }
}

/**
 * @link http://redis.io/commands/select
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ConnectionSelect extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SELECT';
    }
}

/**
 * @link http://redis.io/commands/hdel
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class HashDelete extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'HDEL';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        return self::normalizeVariadic($arguments);
    }
}

/**
 * @link http://redis.io/commands/hexists
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class HashExists extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'HEXISTS';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        return (bool) $data;
    }
}

/**
 * @link http://redis.io/commands/quit
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ConnectionQuit extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'QUIT';
    }
}

/**
 * @link http://redis.io/commands/ping
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ConnectionPing extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'PING';
    }
}

/**
 * @link http://redis.io/commands/auth
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ConnectionAuth extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'AUTH';
    }
}

/**
 * @link http://redis.io/commands/echo
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ConnectionEcho extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'ECHO';
    }
}

/**
 * @link http://redis.io/commands/hget
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class HashGet extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'HGET';
    }
}

/**
 * @link http://redis.io/commands/hgetall
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class HashGetAll extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'HGETALL';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        $result = array();

        for ($i = 0; $i < count($data); $i++) {
            $result[$data[$i]] = $data[++$i];
        }

        return $result;
    }
}

/**
 * @link http://redis.io/commands/hlen
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class HashLength extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'HLEN';
    }
}

/**
 * @link http://redis.io/commands/hscan
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class HashScan extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'HSCAN';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        if (count($arguments) === 3 && is_array($arguments[2])) {
            $options = $this->prepareOptions(array_pop($arguments));
            $arguments = array_merge($arguments, $options);
        }

        return $arguments;
    }

    /**
     * Returns a list of options and modifiers compatible with Redis.
     *
     * @param array $options List of options.
     *
     * @return array
     */
    protected function prepareOptions($options)
    {
        $options = array_change_key_case($options, CASE_UPPER);
        $normalized = array();

        if (!empty($options['MATCH'])) {
            $normalized[] = 'MATCH';
            $normalized[] = $options['MATCH'];
        }

        if (!empty($options['COUNT'])) {
            $normalized[] = 'COUNT';
            $normalized[] = $options['COUNT'];
        }

        return $normalized;
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        if (is_array($data)) {
            $fields = $data[1];
            $result = array();

            for ($i = 0; $i < count($fields); $i++) {
                $result[$fields[$i]] = $fields[++$i];
            }

            $data[1] = $result;
        }

        return $data;
    }
}

/**
 * @link http://redis.io/commands/hset
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class HashSet extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'HSET';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        return (bool) $data;
    }
}

/**
 * @link http://redis.io/commands/hkeys
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class HashKeys extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'HKEYS';
    }
}

/**
 * @link http://redis.io/commands/hincrbyfloat
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class HashIncrementByFloat extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'HINCRBYFLOAT';
    }
}

/**
 * @link http://redis.io/commands/hmget
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class HashGetMultiple extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'HMGET';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        return self::normalizeVariadic($arguments);
    }
}

/**
 * @link http://redis.io/commands/hincrby
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class HashIncrementBy extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'HINCRBY';
    }
}

/**
 * @link http://redis.io/commands/scan
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class KeyScan extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SCAN';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        if (count($arguments) === 2 && is_array($arguments[1])) {
            $options = $this->prepareOptions(array_pop($arguments));
            $arguments = array_merge($arguments, $options);
        }

        return $arguments;
    }

    /**
     * Returns a list of options and modifiers compatible with Redis.
     *
     * @param array $options List of options.
     *
     * @return array
     */
    protected function prepareOptions($options)
    {
        $options = array_change_key_case($options, CASE_UPPER);
        $normalized = array();

        if (!empty($options['MATCH'])) {
            $normalized[] = 'MATCH';
            $normalized[] = $options['MATCH'];
        }

        if (!empty($options['COUNT'])) {
            $normalized[] = 'COUNT';
            $normalized[] = $options['COUNT'];
        }

        return $normalized;
    }
}

/**
 * @link http://redis.io/commands/sort
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class KeySort extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'SORT';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        if (count($arguments) === 1) {
            return $arguments;
        }

        $query = array($arguments[0]);
        $sortParams = array_change_key_case($arguments[1], CASE_UPPER);

        if (isset($sortParams['BY'])) {
            $query[] = 'BY';
            $query[] = $sortParams['BY'];
        }

        if (isset($sortParams['GET'])) {
            $getargs = $sortParams['GET'];

            if (is_array($getargs)) {
                foreach ($getargs as $getarg) {
                    $query[] = 'GET';
                    $query[] = $getarg;
                }
            } else {
                $query[] = 'GET';
                $query[] = $getargs;
            }
        }

        if (isset($sortParams['LIMIT']) &&
            is_array($sortParams['LIMIT']) &&
            count($sortParams['LIMIT']) == 2) {

            $query[] = 'LIMIT';
            $query[] = $sortParams['LIMIT'][0];
            $query[] = $sortParams['LIMIT'][1];
        }

        if (isset($sortParams['SORT'])) {
            $query[] = strtoupper($sortParams['SORT']);
        }

        if (isset($sortParams['ALPHA']) && $sortParams['ALPHA'] == true) {
            $query[] = 'ALPHA';
        }

        if (isset($sortParams['STORE'])) {
            $query[] = 'STORE';
            $query[] = $sortParams['STORE'];
        }

        return $query;
    }
}

/**
 * Class for generic "anonymous" Redis commands.
 *
 * This command class does not filter input arguments or parse responses, but
 * can be used to leverage the standard Predis API to execute any command simply
 * by providing the needed arguments following the command signature as defined
 * by Redis in its documentation.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class RawCommand implements CommandInterface
{
    private $slot;
    private $commandID;
    private $arguments;

    /**
     * @param array $arguments Command ID and its arguments.
     *
     * @throws \InvalidArgumentException
     */
    public function __construct(array $arguments)
    {
        if (!$arguments) {
            throw new InvalidArgumentException(
                'The arguments array must contain at least the command ID.'
            );
        }

        $this->commandID = strtoupper(array_shift($arguments));
        $this->arguments = $arguments;
    }

    /**
     * Creates a new raw command using a variadic method.
     *
     * @param string $commandID Redis command ID.
     * @param string ...        Arguments list for the command.
     *
     * @return CommandInterface
     */
    public static function create($commandID /* [ $arg, ... */)
    {
        $arguments = func_get_args();
        $command = new self($arguments);

        return $command;
    }

    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return $this->commandID;
    }

    /**
     * {@inheritdoc}
     */
    public function setArguments(array $arguments)
    {
        $this->arguments = $arguments;
        unset($this->slot);
    }

    /**
     * {@inheritdoc}
     */
    public function setRawArguments(array $arguments)
    {
        $this->setArguments($arguments);
    }

    /**
     * {@inheritdoc}
     */
    public function getArguments()
    {
        return $this->arguments;
    }

    /**
     * {@inheritdoc}
     */
    public function getArgument($index)
    {
        if (isset($this->arguments[$index])) {
            return $this->arguments[$index];
        }
    }

    /**
     * {@inheritdoc}
     */
    public function setSlot($slot)
    {
        $this->slot = $slot;
    }

    /**
     * {@inheritdoc}
     */
    public function getSlot()
    {
        if (isset($this->slot)) {
            return $this->slot;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        return $data;
    }
}

/**
 * Base class used to implement an higher level abstraction for commands based
 * on Lua scripting with EVAL and EVALSHA.
 *
 * @link http://redis.io/commands/eval
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
abstract class ScriptCommand extends ServerEvalSHA
{
    /**
     * Gets the body of a Lua script.
     *
     * @return string
     */
    abstract public function getScript();

    /**
     * Specifies the number of arguments that should be considered as keys.
     *
     * The default behaviour for the base class is to return 0 to indicate that
     * all the elements of the arguments array should be considered as keys, but
     * subclasses can enforce a static number of keys.
     *
     * @return int
     */
    protected function getKeysCount()
    {
        return 0;
    }

    /**
     * Returns the elements from the arguments that are identified as keys.
     *
     * @return array
     */
    public function getKeys()
    {
        return array_slice($this->getArguments(), 2, $this->getKeysCount());
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        if (($numkeys = $this->getKeysCount()) && $numkeys < 0) {
            $numkeys = count($arguments) + $numkeys;
        }

        return array_merge(array(sha1($this->getScript()), (int) $numkeys), $arguments);
    }

    /**
     * @return array
     */
    public function getEvalArguments()
    {
        $arguments = $this->getArguments();
        $arguments[0] = $this->getScript();

        return $arguments;
    }
}

/**
 * @link http://redis.io/commands/bgrewriteaof
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerBackgroundRewriteAOF extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'BGREWRITEAOF';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        return $data == 'Background append only file rewriting started';
    }
}

/**
 * @link http://redis.io/commands/punsubscribe
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class PubSubUnsubscribeByPattern extends PubSubUnsubscribe
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'PUNSUBSCRIBE';
    }
}

/**
 * @link http://redis.io/commands/psubscribe
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class PubSubSubscribeByPattern extends PubSubSubscribe
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'PSUBSCRIBE';
    }
}

/**
 * @link http://redis.io/commands/publish
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class PubSubPublish extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'PUBLISH';
    }
}

/**
 * @link http://redis.io/commands/pubsub
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class PubSubPubsub extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'PUBSUB';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        switch (strtolower($this->getArgument(0))) {
            case 'numsub':
                return self::processNumsub($data);

            default:
                return $data;
        }
    }

    /**
     * Returns the processed response to PUBSUB NUMSUB.
     *
     * @param array $channels List of channels
     *
     * @return array
     */
    protected static function processNumsub(array $channels)
    {
        $processed = array();
        $count = count($channels);

        for ($i = 0; $i < $count; $i++) {
            $processed[$channels[$i]] = $channels[++$i];
        }

        return $processed;
    }
}

/**
 * @link http://redis.io/commands/bgsave
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerBackgroundSave extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'BGSAVE';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        return $data === 'Background saving started' ? true : $data;
    }
}

/**
 * @link http://redis.io/commands/client-list
 * @link http://redis.io/commands/client-kill
 * @link http://redis.io/commands/client-getname
 * @link http://redis.io/commands/client-setname
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerClient extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'CLIENT';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        $args = array_change_key_case($this->getArguments(), CASE_UPPER);

        switch (strtoupper($args[0])) {
            case 'LIST':
                return $this->parseClientList($data);
            case 'KILL':
            case 'GETNAME':
            case 'SETNAME':
            default:
                return $data;
        }
    }

    /**
     * Parses the response to CLIENT LIST and returns a structured list.
     *
     * @param string $data Response buffer.
     *
     * @return array
     */
    protected function parseClientList($data)
    {
        $clients = array();

        foreach (explode("\n", $data, -1) as $clientData) {
            $client = array();

            foreach (explode(' ', $clientData) as $kv) {
                @list($k, $v) = explode('=', $kv);
                $client[$k] = $v;
            }

            $clients[] = $client;
        }

        return $clients;
    }
}

/**
 * @link http://redis.io/commands/info
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerInfoV26x extends ServerInfo
{
    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        if ($data === '') {
            return array();
        }

        $info = array();

        $current = null;
        $infoLines = preg_split('/\r?\n/', $data);

        if (isset($infoLines[0]) && $infoLines[0][0] !== '#') {
            return parent::parseResponse($data);
        }

        foreach ($infoLines as $row) {
            if ($row === '') {
                continue;
            }

            if (preg_match('/^# (\w+)$/', $row, $matches)) {
                $info[$matches[1]] = array();
                $current = &$info[$matches[1]];
                continue;
            }

            list($k, $v) = $this->parseRow($row);
            $current[$k] = $v;
        }

        return $info;
    }
}

/**
 * @link http://redis.io/commands/lastsave
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerLastSave extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'LASTSAVE';
    }
}

/**
 * @link http://redis.io/commands/monitor
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerMonitor extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'MONITOR';
    }
}

/**
 * @link http://redis.io/commands/flushdb
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerFlushDatabase extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'FLUSHDB';
    }
}

/**
 * @link http://redis.io/commands/dbsize
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerDatabaseSize extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'DBSIZE';
    }
}

/**
 * @link http://redis.io/commands/command
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerCommand extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'COMMAND';
    }
}

/**
 * @link http://redis.io/commands/config-set
 * @link http://redis.io/commands/config-get
 * @link http://redis.io/commands/config-resetstat
 * @link http://redis.io/commands/config-rewrite
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerConfig extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'CONFIG';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        if (is_array($data)) {
            $result = array();

            for ($i = 0; $i < count($data); $i++) {
                $result[$data[$i]] = $data[++$i];
            }

            return $result;
        }

        return $data;
    }
}

/**
 * Defines a command whose keys can be prefixed.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface PrefixableCommandInterface extends CommandInterface
{
    /**
     * Prefixes all the keys found in the arguments of the command.
     *
     * @param string $prefix String used to prefix the keys.
     */
    public function prefixKeys($prefix);
}

/**
 * @link http://redis.io/commands/ltrim
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ListTrim extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'LTRIM';
    }
}

/**
 * @link http://redis.io/commands/lpop
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ListPopFirst extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'LPOP';
    }
}

/**
 * @link http://redis.io/commands/rpop
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ListPopLast extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'RPOP';
    }
}

/**
 * @link http://redis.io/commands/brpop
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ListPopLastBlocking extends ListPopFirstBlocking
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'BRPOP';
    }
}

/**
 * @link http://redis.io/commands/llen
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ListLength extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'LLEN';
    }
}

/**
 * @link http://redis.io/commands/linsert
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ListInsert extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'LINSERT';
    }
}

/**
 * @link http://redis.io/commands/type
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class KeyType extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'TYPE';
    }
}

/**
 * @link http://redis.io/commands/lindex
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ListIndex extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'LINDEX';
    }
}

/**
 * @link http://redis.io/commands/rpoplpush
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ListPopLastPushHead extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'RPOPLPUSH';
    }
}

/**
 * @link http://redis.io/commands/brpoplpush
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ListPopLastPushHeadBlocking extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'BRPOPLPUSH';
    }
}

/**
 * @link http://redis.io/commands/lrem
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ListRemove extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'LREM';
    }
}

/**
 * @link http://redis.io/commands/lset
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ListSet extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'LSET';
    }
}

/**
 * @link http://redis.io/commands/lrange
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ListRange extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'LRANGE';
    }
}

/**
 * @link http://redis.io/commands/rpushx
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ListPushTailX extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'RPUSHX';
    }
}

/**
 * @link http://redis.io/commands/lpush
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ListPushHead extends ListPushTail
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'LPUSH';
    }
}

/**
 * @link http://redis.io/commands/lpushx
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ListPushHeadX extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'LPUSHX';
    }
}

/**
 * @link http://redis.io/commands/object
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerObject extends Command
{
    /**
     * {@inheritdoc}
     */
    public function getId()
    {
        return 'OBJECT';
    }
}

/* --------------------------------------------------------------------------- */

namespace Predis\Connection;

use InvalidArgumentException;
use Predis\CommunicationException;
use Predis\Command\CommandInterface;
use Predis\Protocol\ProtocolException;
use Predis\Protocol\ProtocolProcessorInterface;
use Predis\Protocol\Text\ProtocolProcessor as TextProtocolProcessor;
use UnexpectedValueException;
use ReflectionClass;
use Predis\Command\RawCommand;
use Predis\NotSupportedException;
use Predis\Response\Error as ErrorResponse;
use Predis\Response\Status as StatusResponse;

/**
 * Defines a connection object used to communicate with one or multiple
 * Redis servers.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface ConnectionInterface
{
    /**
     * Opens the connection to Redis.
     */
    public function connect();

    /**
     * Closes the connection to Redis.
     */
    public function disconnect();

    /**
     * Checks if the connection to Redis is considered open.
     *
     * @return bool
     */
    public function isConnected();

    /**
     * Writes the request for the given command over the connection.
     *
     * @param CommandInterface $command Command instance.
     */
    public function writeRequest(CommandInterface $command);

    /**
     * Reads the response to the given command from the connection.
     *
     * @param CommandInterface $command Command instance.
     *
     * @return mixed
     */
    public function readResponse(CommandInterface $command);

    /**
     * Writes a request for the given command over the connection and reads back
     * the response returned by Redis.
     *
     * @param CommandInterface $command Command instance.
     *
     * @return mixed
     */
    public function executeCommand(CommandInterface $command);
}

/**
 * Defines a connection used to communicate with a single Redis node.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface NodeConnectionInterface extends ConnectionInterface
{
    /**
     * Returns a string representation of the connection.
     *
     * @return string
     */
    public function __toString();

    /**
     * Returns the underlying resource used to communicate with Redis.
     *
     * @return mixed
     */
    public function getResource();

    /**
     * Returns the parameters used to initialize the connection.
     *
     * @return ParametersInterface
     */
    public function getParameters();

    /**
     * Pushes the given command into a queue of commands executed when
     * establishing the actual connection to Redis.
     *
     * @param CommandInterface $command Instance of a Redis command.
     */
    public function addConnectCommand(CommandInterface $command);

    /**
     * Reads a response from the server.
     *
     * @return mixed
     */
    public function read();
}

/**
 * Defines a virtual connection composed of multiple connection instances to
 * single Redis nodes.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface AggregateConnectionInterface extends ConnectionInterface
{
    /**
     * Adds a connection instance to the aggregate connection.
     *
     * @param NodeConnectionInterface $connection Connection instance.
     */
    public function add(NodeConnectionInterface $connection);

    /**
     * Removes the specified connection instance from the aggregate connection.
     *
     * @param NodeConnectionInterface $connection Connection instance.
     *
     * @return bool Returns true if the connection was in the pool.
     */
    public function remove(NodeConnectionInterface $connection);

    /**
     * Returns the connection instance in charge for the given command.
     *
     * @param CommandInterface $command Command instance.
     *
     * @return NodeConnectionInterface
     */
    public function getConnection(CommandInterface $command);

    /**
     * Returns a connection instance from the aggregate connection by its alias.
     *
     * @param string $connectionID Connection alias.
     *
     * @return NodeConnectionInterface|null
     */
    public function getConnectionById($connectionID);
}

/**
 * Base class with the common logic used by connection classes to communicate
 * with Redis.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
abstract class AbstractConnection implements NodeConnectionInterface
{
    private $resource;
    private $cachedId;

    protected $parameters;
    protected $initCommands = array();

    /**
     * @param ParametersInterface $parameters Initialization parameters for the connection.
     */
    public function __construct(ParametersInterface $parameters)
    {
        $this->parameters = $this->assertParameters($parameters);
    }

    /**
     * Disconnects from the server and destroys the underlying resource when
     * PHP's garbage collector kicks in.
     */
    public function __destruct()
    {
        $this->disconnect();
    }

    /**
     * Checks some of the parameters used to initialize the connection.
     *
     * @param ParametersInterface $parameters Initialization parameters for the connection.
     *
     * @return ParametersInterface
     *
     * @throws \InvalidArgumentException
     */
    protected function assertParameters(ParametersInterface $parameters)
    {
        $scheme = $parameters->scheme;

        if ($scheme !== 'tcp' && $scheme !== 'unix') {
            throw new InvalidArgumentException("Invalid scheme: '$scheme'.");
        }

        if ($scheme === 'unix' && !isset($parameters->path)) {
            throw new InvalidArgumentException('Missing UNIX domain socket path.');
        }

        return $parameters;
    }

    /**
     * Creates the underlying resource used to communicate with Redis.
     *
     * @return mixed
     */
    abstract protected function createResource();

    /**
     * {@inheritdoc}
     */
    public function isConnected()
    {
        return isset($this->resource);
    }

    /**
     * {@inheritdoc}
     */
    public function connect()
    {
        if (!$this->isConnected()) {
            $this->resource = $this->createResource();

            return true;
        }

        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function disconnect()
    {
        unset($this->resource);
    }

    /**
     * {@inheritdoc}
     */
    public function addConnectCommand(CommandInterface $command)
    {
        $this->initCommands[] = $command;
    }

    /**
     * {@inheritdoc}
     */
    public function executeCommand(CommandInterface $command)
    {
        $this->writeRequest($command);

        return $this->readResponse($command);
    }

    /**
     * {@inheritdoc}
     */
    public function readResponse(CommandInterface $command)
    {
        return $this->read();
    }

    /**
     * Helper method to handle connection errors.
     *
     * @param string $message Error message.
     * @param int    $code    Error code.
     */
    protected function onConnectionError($message, $code = null)
    {
        CommunicationException::handle(
            new ConnectionException(
                $this, "$message [{$this->parameters->scheme}://{$this->getIdentifier()}]", $code
            )
        );
    }

    /**
     * Helper method to handle protocol errors.
     *
     * @param string $message Error message.
     */
    protected function onProtocolError($message)
    {
        CommunicationException::handle(
            new ProtocolException(
                $this, "$message [{$this->parameters->scheme}://{$this->getIdentifier()}]"
            )
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getResource()
    {
        if (isset($this->resource)) {
            return $this->resource;
        }

        $this->connect();

        return $this->resource;
    }

    /**
     * {@inheritdoc}
     */
    public function getParameters()
    {
        return $this->parameters;
    }

    /**
     * Gets an identifier for the connection.
     *
     * @return string
     */
    protected function getIdentifier()
    {
        if ($this->parameters->scheme === 'unix') {
            return $this->parameters->path;
        }

        return "{$this->parameters->host}:{$this->parameters->port}";
    }

    /**
     * {@inheritdoc}
     */
    public function __toString()
    {
        if (!isset($this->cachedId)) {
            $this->cachedId = $this->getIdentifier();
        }

        return $this->cachedId;
    }

    /**
     * {@inheritdoc}
     */
    public function __sleep()
    {
        return array('parameters', 'initCommands');
    }
}

/**
 * Standard connection to Redis servers implemented on top of PHP's streams.
 * The connection parameters supported by this class are:
 *
 *  - scheme: it can be either 'tcp' or 'unix'.
 *  - host: hostname or IP address of the server.
 *  - port: TCP port of the server.
 *  - path: path of a UNIX domain socket when scheme is 'unix'.
 *  - timeout: timeout to perform the connection.
 *  - read_write_timeout: timeout of read / write operations.
 *  - async_connect: performs the connection asynchronously.
 *  - tcp_nodelay: enables or disables Nagle's algorithm for coalescing.
 *  - persistent: the connection is left intact after a GC collection.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StreamConnection extends AbstractConnection
{
    /**
     * Disconnects from the server and destroys the underlying resource when the
     * garbage collector kicks in only if the connection has not been marked as
     * persistent.
     */
    public function __destruct()
    {
        if (isset($this->parameters->persistent) && $this->parameters->persistent) {
            return;
        }

        $this->disconnect();
    }

    /**
     * {@inheritdoc}
     */
    protected function createResource()
    {
        $initializer = "{$this->parameters->scheme}StreamInitializer";
        $resource = $this->$initializer($this->parameters);

        return $resource;
    }

    /**
     * Initializes a TCP stream resource.
     *
     * @param ParametersInterface $parameters Initialization parameters for the connection.
     *
     * @return resource
     */
    protected function tcpStreamInitializer(ParametersInterface $parameters)
    {
        $uri = "tcp://{$parameters->host}:{$parameters->port}";
        $flags = STREAM_CLIENT_CONNECT;

        if (isset($parameters->async_connect) && (bool) $parameters->async_connect) {
            $flags |= STREAM_CLIENT_ASYNC_CONNECT;
        }

        if (isset($parameters->persistent) && (bool) $parameters->persistent) {
            $flags |= STREAM_CLIENT_PERSISTENT;
            $uri .= strpos($path = $parameters->path, '/') === 0 ? $path : "/$path";
        }

        $resource = @stream_socket_client($uri, $errno, $errstr, (float) $parameters->timeout, $flags);

        if (!$resource) {
            $this->onConnectionError(trim($errstr), $errno);
        }

        if (isset($parameters->read_write_timeout)) {
            $rwtimeout = (float) $parameters->read_write_timeout;
            $rwtimeout = $rwtimeout > 0 ? $rwtimeout : -1;
            $timeoutSeconds  = floor($rwtimeout);
            $timeoutUSeconds = ($rwtimeout - $timeoutSeconds) * 1000000;
            stream_set_timeout($resource, $timeoutSeconds, $timeoutUSeconds);
        }

        if (isset($parameters->tcp_nodelay) && function_exists('socket_import_stream')) {
            $socket = socket_import_stream($resource);
            socket_set_option($socket, SOL_TCP, TCP_NODELAY, (int) $parameters->tcp_nodelay);
        }

        return $resource;
    }

    /**
     * Initializes a UNIX stream resource.
     *
     * @param ParametersInterface $parameters Initialization parameters for the connection.
     *
     * @return resource
     */
    protected function unixStreamInitializer(ParametersInterface $parameters)
    {
        $uri = "unix://{$parameters->path}";
        $flags = STREAM_CLIENT_CONNECT;

        if ((bool) $parameters->persistent) {
            $flags |= STREAM_CLIENT_PERSISTENT;
        }

        $resource = @stream_socket_client($uri, $errno, $errstr, (float) $parameters->timeout, $flags);

        if (!$resource) {
            $this->onConnectionError(trim($errstr), $errno);
        }

        if (isset($parameters->read_write_timeout)) {
            $rwtimeout = (float) $parameters->read_write_timeout;
            $rwtimeout = $rwtimeout > 0 ? $rwtimeout : -1;
            $timeoutSeconds  = floor($rwtimeout);
            $timeoutUSeconds = ($rwtimeout - $timeoutSeconds) * 1000000;
            stream_set_timeout($resource, $timeoutSeconds, $timeoutUSeconds);
        }

        return $resource;
    }

    /**
     * {@inheritdoc}
     */
    public function connect()
    {
        if (parent::connect() && $this->initCommands) {
            foreach ($this->initCommands as $command) {
                $this->executeCommand($command);
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function disconnect()
    {
        if ($this->isConnected()) {
            fclose($this->getResource());
            parent::disconnect();
        }
    }

    /**
     * Performs a write operation over the stream of the buffer containing a
     * command serialized with the Redis wire protocol.
     *
     * @param string $buffer Representation of a command in the Redis wire protocol.
     */
    protected function write($buffer)
    {
        $socket = $this->getResource();

        while (($length = strlen($buffer)) > 0) {
            $written = @fwrite($socket, $buffer);

            if ($length === $written) {
                return;
            }

            if ($written === false || $written === 0) {
                $this->onConnectionError('Error while writing bytes to the server.');
            }

            $buffer = substr($buffer, $written);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function read()
    {
        $socket = $this->getResource();
        $chunk = fgets($socket);

        if ($chunk === false || $chunk === '') {
            $this->onConnectionError('Error while reading line from the server.');
        }

        $prefix = $chunk[0];
        $payload = substr($chunk, 1, -2);

        switch ($prefix) {
            case '+':
                return StatusResponse::get($payload);

            case '$':
                $size = (int) $payload;

                if ($size === -1) {
                    return null;
                }

                $bulkData = '';
                $bytesLeft = ($size += 2);

                do {
                    $chunk = fread($socket, min($bytesLeft, 4096));

                    if ($chunk === false || $chunk === '') {
                        $this->onConnectionError('Error while reading bytes from the server.');
                    }

                    $bulkData .= $chunk;
                    $bytesLeft = $size - strlen($bulkData);
                } while ($bytesLeft > 0);

                return substr($bulkData, 0, -2);

            case '*':
                $count = (int) $payload;

                if ($count === -1) {
                    return null;
                }

                $multibulk = array();

                for ($i = 0; $i < $count; $i++) {
                    $multibulk[$i] = $this->read();
                }

                return $multibulk;

            case ':':
                return (int) $payload;

            case '-':
                return new ErrorResponse($payload);

            default:
                $this->onProtocolError("Unknown response prefix: '$prefix'.");

                return;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function writeRequest(CommandInterface $command)
    {
        $commandID = $command->getId();
        $arguments = $command->getArguments();

        $cmdlen = strlen($commandID);
        $reqlen = count($arguments) + 1;

        $buffer = "*{$reqlen}\r\n\${$cmdlen}\r\n{$commandID}\r\n";

        for ($i = 0, $reqlen--; $i < $reqlen; $i++) {
            $argument = $arguments[$i];
            $arglen = strlen($argument);
            $buffer .= "\${$arglen}\r\n{$argument}\r\n";
        }

        $this->write($buffer);
    }
}

/**
 * Interface defining a container for connection parameters.
 *
 * The actual list of connection parameters depends on the features supported by
 * each connection backend class (please refer to their specific documentation),
 * but the most common parameters used through the library are:
 *
 * @property-read string scheme             Connection scheme, such as 'tcp' or 'unix'.
 * @property-read string host               IP address or hostname of Redis.
 * @property-read int    port               TCP port on which Redis is listening to.
 * @property-read string path               Path of a UNIX domain socket file.
 * @property-read string alias              Alias for the connection.
 * @property-read float  timeout            Timeout for the connect() operation.
 * @property-read float  read_write_timeout Timeout for read() and write() operations.
 * @property-read bool   async_connect      Performs the connect() operation asynchronously.
 * @property-read bool   tcp_nodelay        Toggles the Nagle's algorithm for coalescing.
 * @property-read bool   persistent         Leaves the connection open after a GC collection.
 * @property-read string password           Password to access Redis (see the AUTH command).
 * @property-read string database           Database index (see the SELECT command).
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface ParametersInterface
{
    /**
     * Checks if the specified parameters is set.
     *
     * @param string $parameter Name of the parameter.
     *
     * @return bool
     */
    public function __isset($parameter);

    /**
     * Returns the value of the specified parameter.
     *
     * @param string $parameter Name of the parameter.
     *
     * @return mixed|null
     */
    public function __get($parameter);

    /**
     * Returns an array representation of the connection parameters.
     *
     * @return array
     */
    public function toArray();
}

/**
 * Interface for classes providing a factory of connections to Redis nodes.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface FactoryInterface
{
    /**
     * Defines or overrides the connection class identified by a scheme prefix.
     *
     * @param string $scheme      Target connection scheme.
     * @param mixed  $initializer Fully-qualified name of a class or a callable for lazy initialization.
     */
    public function define($scheme, $initializer);

    /**
     * Undefines the connection identified by a scheme prefix.
     *
     * @param string $scheme Target connection scheme.
     */
    public function undefine($scheme);

    /**
     * Creates a new connection object.
     *
     * @param mixed $parameters Initialization parameters for the connection.
     *
     * @return NodeConnectionInterface
     */
    public function create($parameters);

    /**
     * Aggregates single connections into an aggregate connection instance.
     *
     * @param AggregateConnectionInterface $aggregate  Aggregate connection instance.
     * @param array                        $parameters List of parameters for each connection.
     */
    public function aggregate(AggregateConnectionInterface $aggregate, array $parameters);
}

/**
 * Defines a connection to communicate with a single Redis server that leverages
 * an external protocol processor to handle pluggable protocol handlers.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface CompositeConnectionInterface extends NodeConnectionInterface
{
    /**
     * Returns the protocol processor used by the connection.
     */
    public function getProtocol();

    /**
     * Writes the buffer containing over the connection.
     *
     * @param string $buffer String buffer to be sent over the connection.
     */
    public function writeBuffer($buffer);

    /**
     * Reads the given number of bytes from the connection.
     *
     * @param int $length Number of bytes to read from the connection.
     *
     * @return string
     */
    public function readBuffer($length);

    /**
     * Reads a line from the connection.
     *
     * @param string
     */
    public function readLine();
}

/**
 * This class provides the implementation of a Predis connection that uses PHP's
 * streams for network communication and wraps the phpiredis C extension (PHP
 * bindings for hiredis) to parse and serialize the Redis protocol.
 *
 * This class is intended to provide an optional low-overhead alternative for
 * processing responses from Redis compared to the standard pure-PHP classes.
 * Differences in speed when dealing with short inline responses are practically
 * nonexistent, the actual speed boost is for big multibulk responses when this
 * protocol processor can parse and return responses very fast.
 *
 * For instructions on how to build and install the phpiredis extension, please
 * consult the repository of the project.
 *
 * The connection parameters supported by this class are:
 *
 *  - scheme: it can be either 'tcp' or 'unix'.
 *  - host: hostname or IP address of the server.
 *  - port: TCP port of the server.
 *  - path: path of a UNIX domain socket when scheme is 'unix'.
 *  - timeout: timeout to perform the connection.
 *  - read_write_timeout: timeout of read / write operations.
 *  - async_connect: performs the connection asynchronously.
 *  - tcp_nodelay: enables or disables Nagle's algorithm for coalescing.
 *  - persistent: the connection is left intact after a GC collection.
 *
 * @link https://github.com/nrk/phpiredis
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class PhpiredisStreamConnection extends StreamConnection
{
    private $reader;

    /**
     * {@inheritdoc}
     */
    public function __construct(ParametersInterface $parameters)
    {
        $this->assertExtensions();

        parent::__construct($parameters);

        $this->reader = $this->createReader();
    }

    /**
     * {@inheritdoc}
     */
    public function __destruct()
    {
        phpiredis_reader_destroy($this->reader);

        parent::__destruct();
    }

    /**
     * Checks if the phpiredis extension is loaded in PHP.
     */
    private function assertExtensions()
    {
        if (!extension_loaded('phpiredis')) {
            throw new NotSupportedException(
                'The "phpiredis" extension is required by this connection backend.'
            );
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function tcpStreamInitializer(ParametersInterface $parameters)
    {
        $uri = "tcp://{$parameters->host}:{$parameters->port}";
        $flags = STREAM_CLIENT_CONNECT;
        $socket = null;

        if (isset($parameters->async_connect) && (bool) $parameters->async_connect) {
            $flags |= STREAM_CLIENT_ASYNC_CONNECT;
        }

        if (isset($parameters->persistent) && (bool) $parameters->persistent) {
            $flags |= STREAM_CLIENT_PERSISTENT;
            $uri .= strpos($path = $parameters->path, '/') === 0 ? $path : "/$path";
        }

        $resource = @stream_socket_client($uri, $errno, $errstr, (float) $parameters->timeout, $flags);

        if (!$resource) {
            $this->onConnectionError(trim($errstr), $errno);
        }

        if (isset($parameters->read_write_timeout) && function_exists('socket_import_stream')) {
            $rwtimeout = (float) $parameters->read_write_timeout;
            $rwtimeout = $rwtimeout > 0 ? $rwtimeout : -1;

            $timeout = array(
                'sec'  => $timeoutSeconds = floor($rwtimeout),
                'usec' => ($rwtimeout - $timeoutSeconds) * 1000000,
            );

            $socket = $socket ?: socket_import_stream($resource);
            @socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, $timeout);
            @socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, $timeout);
        }

        if (isset($parameters->tcp_nodelay) && function_exists('socket_import_stream')) {
            $socket = $socket ?: socket_import_stream($resource);
            socket_set_option($socket, SOL_TCP, TCP_NODELAY, (int) $parameters->tcp_nodelay);
        }

        return $resource;
    }

    /**
     * Creates a new instance of the protocol reader resource.
     *
     * @return resource
     */
    private function createReader()
    {
        $reader = phpiredis_reader_create();

        phpiredis_reader_set_status_handler($reader, $this->getStatusHandler());
        phpiredis_reader_set_error_handler($reader, $this->getErrorHandler());

        return $reader;
    }

    /**
     * Returns the underlying protocol reader resource.
     *
     * @return resource
     */
    protected function getReader()
    {
        return $this->reader;
    }

    /**
     * Returns the handler used by the protocol reader for inline responses.
     *
     * @return \Closure
     */
    protected function getStatusHandler()
    {
        return function ($payload) {
            return StatusResponse::get($payload);
        };
    }

    /**
     * Returns the handler used by the protocol reader for error responses.
     *
     * @return \Closure
     */
    protected function getErrorHandler()
    {
        return function ($errorMessage) {
            return new ErrorResponse($errorMessage);
        };
    }

    /**
     * {@inheritdoc}
     */
    public function read()
    {
        $socket = $this->getResource();
        $reader = $this->reader;

        while (PHPIREDIS_READER_STATE_INCOMPLETE === $state = phpiredis_reader_get_state($reader)) {
            $buffer = stream_socket_recvfrom($socket, 4096);

            if ($buffer === false || $buffer === '') {
                $this->onConnectionError('Error while reading bytes from the server.');
            }

            phpiredis_reader_feed($reader, $buffer);
        }

        if ($state === PHPIREDIS_READER_STATE_COMPLETE) {
            return phpiredis_reader_get_reply($reader);
        } else {
            $this->onProtocolError(phpiredis_reader_get_error($reader));

            return;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function writeRequest(CommandInterface $command)
    {
        $arguments = $command->getArguments();
        array_unshift($arguments, $command->getId());

        $this->write(phpiredis_format_command($arguments));
    }

    /**
     * {@inheritdoc}
     */
    public function __wakeup()
    {
        $this->assertExtensions();
        $this->reader = $this->createReader();
    }
}

/**
 * This class implements a Predis connection that actually talks with Webdis
 * instead of connecting directly to Redis. It relies on the cURL extension to
 * communicate with the web server and the phpiredis extension to parse the
 * protocol for responses returned in the http response bodies.
 *
 * Some features are not yet available or they simply cannot be implemented:
 *   - Pipelining commands.
 *   - Publish / Subscribe.
 *   - MULTI / EXEC transactions (not yet supported by Webdis).
 *
 * The connection parameters supported by this class are:
 *
 *  - scheme: must be 'http'.
 *  - host: hostname or IP address of the server.
 *  - port: TCP port of the server.
 *  - timeout: timeout to perform the connection.
 *  - user: username for authentication.
 *  - pass: password for authentication.
 *
 * @link http://webd.is
 * @link http://github.com/nicolasff/webdis
 * @link http://github.com/seppo0010/phpiredis
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class WebdisConnection implements NodeConnectionInterface
{
    private $parameters;
    private $resource;
    private $reader;

    /**
     * @param ParametersInterface $parameters Initialization parameters for the connection.
     *
     * @throws \InvalidArgumentException
     */
    public function __construct(ParametersInterface $parameters)
    {
        $this->assertExtensions();

        if ($parameters->scheme !== 'http') {
            throw new InvalidArgumentException("Invalid scheme: '{$parameters->scheme}'.");
        }

        $this->parameters = $parameters;

        $this->resource = $this->createCurl();
        $this->reader = $this->createReader();
    }

    /**
     * Frees the underlying cURL and protocol reader resources when the garbage
     * collector kicks in.
     */
    public function __destruct()
    {
        curl_close($this->resource);
        phpiredis_reader_destroy($this->reader);
    }

    /**
     * Helper method used to throw on unsupported methods.
     *
     * @param string $method Name of the unsupported method.
     *
     * @throws NotSupportedException
     */
    private function throwNotSupportedException($method)
    {
        $class = __CLASS__;
        throw new NotSupportedException("The method $class::$method() is not supported.");
    }

    /**
     * Checks if the cURL and phpiredis extensions are loaded in PHP.
     */
    private function assertExtensions()
    {
        if (!extension_loaded('curl')) {
            throw new NotSupportedException(
                'The "curl" extension is required by this connection backend.'
            );
        }

        if (!extension_loaded('phpiredis')) {
            throw new NotSupportedException(
                'The "phpiredis" extension is required by this connection backend.'
            );
        }
    }

    /**
     * Initializes cURL.
     *
     * @return resource
     */
    private function createCurl()
    {
        $parameters = $this->getParameters();

        $options = array(
            CURLOPT_FAILONERROR => true,
            CURLOPT_CONNECTTIMEOUT_MS => $parameters->timeout * 1000,
            CURLOPT_URL => "{$parameters->scheme}://{$parameters->host}:{$parameters->port}",
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
            CURLOPT_POST => true,
            CURLOPT_WRITEFUNCTION => array($this, 'feedReader'),
        );

        if (isset($parameters->user, $parameters->pass)) {
            $options[CURLOPT_USERPWD] = "{$parameters->user}:{$parameters->pass}";
        }

        curl_setopt_array($resource = curl_init(), $options);

        return $resource;
    }

    /**
     * Initializes the phpiredis protocol reader.
     *
     * @return resource
     */
    private function createReader()
    {
        $reader = phpiredis_reader_create();

        phpiredis_reader_set_status_handler($reader, $this->getStatusHandler());
        phpiredis_reader_set_error_handler($reader, $this->getErrorHandler());

        return $reader;
    }

    /**
     * Returns the handler used by the protocol reader for inline responses.
     *
     * @return \Closure
     */
    protected function getStatusHandler()
    {
        return function ($payload) {
            return StatusResponse::get($payload);
        };
    }

    /**
     * Returns the handler used by the protocol reader for error responses.
     *
     * @return \Closure
     */
    protected function getErrorHandler()
    {
        return function ($payload) {
            return new ErrorResponse($payload);
        };
    }

    /**
     * Feeds the phpredis reader resource with the data read from the network.
     *
     * @param resource $resource Reader resource.
     * @param string   $buffer   Buffer of data read from a connection.
     *
     * @return int
     */
    protected function feedReader($resource, $buffer)
    {
        phpiredis_reader_feed($this->reader, $buffer);

        return strlen($buffer);
    }

    /**
     * {@inheritdoc}
     */
    public function connect()
    {
        // NOOP
    }

    /**
     * {@inheritdoc}
     */
    public function disconnect()
    {
        // NOOP
    }

    /**
     * {@inheritdoc}
     */
    public function isConnected()
    {
        return true;
    }

    /**
     * Checks if the specified command is supported by this connection class.
     *
     * @param CommandInterface $command Command instance.
     *
     * @return string
     *
     * @throws NotSupportedException
     */
    protected function getCommandId(CommandInterface $command)
    {
        switch ($commandID = $command->getId()) {
            case 'AUTH':
            case 'SELECT':
            case 'MULTI':
            case 'EXEC':
            case 'WATCH':
            case 'UNWATCH':
            case 'DISCARD':
            case 'MONITOR':
                throw new NotSupportedException("Command '$commandID' is not allowed by Webdis.");

            default:
                return $commandID;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function writeRequest(CommandInterface $command)
    {
        $this->throwNotSupportedException(__FUNCTION__);
    }

    /**
     * {@inheritdoc}
     */
    public function readResponse(CommandInterface $command)
    {
        $this->throwNotSupportedException(__FUNCTION__);
    }

    /**
     * {@inheritdoc}
     */
    public function executeCommand(CommandInterface $command)
    {
        $resource = $this->resource;
        $commandId = $this->getCommandId($command);

        if ($arguments = $command->getArguments()) {
            $arguments = implode('/', array_map('urlencode', $arguments));
            $serializedCommand = "$commandId/$arguments.raw";
        } else {
            $serializedCommand = "$commandId.raw";
        }

        curl_setopt($resource, CURLOPT_POSTFIELDS, $serializedCommand);

        if (curl_exec($resource) === false) {
            $error = curl_error($resource);
            $errno = curl_errno($resource);

            throw new ConnectionException($this, trim($error), $errno);
        }

        if (phpiredis_reader_get_state($this->reader) !== PHPIREDIS_READER_STATE_COMPLETE) {
            throw new ProtocolException($this, phpiredis_reader_get_error($this->reader));
        }

        return phpiredis_reader_get_reply($this->reader);
    }

    /**
     * {@inheritdoc}
     */
    public function getResource()
    {
        return $this->resource;
    }

    /**
     * {@inheritdoc}
     */
    public function getParameters()
    {
        return $this->parameters;
    }

    /**
     * {@inheritdoc}
     */
    public function addConnectCommand(CommandInterface $command)
    {
        $this->throwNotSupportedException(__FUNCTION__);
    }

    /**
     * {@inheritdoc}
     */
    public function read()
    {
        $this->throwNotSupportedException(__FUNCTION__);
    }

    /**
     * {@inheritdoc}
     */
    public function __toString()
    {
        return "{$this->parameters->host}:{$this->parameters->port}";
    }

    /**
     * {@inheritdoc}
     */
    public function __sleep()
    {
        return array('parameters');
    }

    /**
     * {@inheritdoc}
     */
    public function __wakeup()
    {
        $this->assertExtensions();

        $this->resource = $this->createCurl();
        $this->reader = $this->createReader();
    }
}

/**
 * This class provides the implementation of a Predis connection that uses the
 * PHP socket extension for network communication and wraps the phpiredis C
 * extension (PHP bindings for hiredis) to parse the Redis protocol.
 *
 * This class is intended to provide an optional low-overhead alternative for
 * processing responses from Redis compared to the standard pure-PHP classes.
 * Differences in speed when dealing with short inline responses are practically
 * nonexistent, the actual speed boost is for big multibulk responses when this
 * protocol processor can parse and return responses very fast.
 *
 * For instructions on how to build and install the phpiredis extension, please
 * consult the repository of the project.
 *
 * The connection parameters supported by this class are:
 *
 *  - scheme: it can be either 'tcp' or 'unix'.
 *  - host: hostname or IP address of the server.
 *  - port: TCP port of the server.
 *  - path: path of a UNIX domain socket when scheme is 'unix'.
 *  - timeout: timeout to perform the connection.
 *  - read_write_timeout: timeout of read / write operations.
 *
 * @link http://github.com/nrk/phpiredis
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class PhpiredisSocketConnection extends AbstractConnection
{
    private $reader;

    /**
     * {@inheritdoc}
     */
    public function __construct(ParametersInterface $parameters)
    {
        $this->assertExtensions();

        parent::__construct($parameters);

        $this->reader = $this->createReader();
    }

    /**
     * Disconnects from the server and destroys the underlying resource and the
     * protocol reader resource when PHP's garbage collector kicks in.
     */
    public function __destruct()
    {
        phpiredis_reader_destroy($this->reader);

        parent::__destruct();
    }

    /**
     * Checks if the socket and phpiredis extensions are loaded in PHP.
     */
    protected function assertExtensions()
    {
        if (!extension_loaded('sockets')) {
            throw new NotSupportedException(
                'The "sockets" extension is required by this connection backend.'
            );
        }

        if (!extension_loaded('phpiredis')) {
            throw new NotSupportedException(
                'The "phpiredis" extension is required by this connection backend.'
            );
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function assertParameters(ParametersInterface $parameters)
    {
        if (isset($parameters->persistent)) {
            throw new NotSupportedException(
                "Persistent connections are not supported by this connection backend."
            );
        }

        return parent::assertParameters($parameters);
    }

    /**
     * Creates a new instance of the protocol reader resource.
     *
     * @return resource
     */
    private function createReader()
    {
        $reader = phpiredis_reader_create();

        phpiredis_reader_set_status_handler($reader, $this->getStatusHandler());
        phpiredis_reader_set_error_handler($reader, $this->getErrorHandler());

        return $reader;
    }

    /**
     * Returns the underlying protocol reader resource.
     *
     * @return resource
     */
    protected function getReader()
    {
        return $this->reader;
    }

    /**
     * Returns the handler used by the protocol reader for inline responses.
     *
     * @return \Closure
     */
    private function getStatusHandler()
    {
        return function ($payload) {
            return StatusResponse::get($payload);
        };
    }

    /**
     * Returns the handler used by the protocol reader for error responses.
     *
     * @return \Closure
     */
    protected function getErrorHandler()
    {
        return function ($payload) {
            return new ErrorResponse($payload);
        };
    }

    /**
     * Helper method used to throw exceptions on socket errors.
     */
    private function emitSocketError()
    {
        $errno = socket_last_error();
        $errstr = socket_strerror($errno);

        $this->disconnect();

        $this->onConnectionError(trim($errstr), $errno);
    }

    /**
     * {@inheritdoc}
     */
    protected function createResource()
    {
        $isUnix = $this->parameters->scheme === 'unix';
        $domain = $isUnix ? AF_UNIX : AF_INET;
        $protocol = $isUnix ? 0 : SOL_TCP;

        $socket = @call_user_func('socket_create', $domain, SOCK_STREAM, $protocol);

        if (!is_resource($socket)) {
            $this->emitSocketError();
        }

        $this->setSocketOptions($socket, $this->parameters);

        return $socket;
    }

    /**
     * Sets options on the socket resource from the connection parameters.
     *
     * @param resource            $socket     Socket resource.
     * @param ParametersInterface $parameters Parameters used to initialize the connection.
     */
    private function setSocketOptions($socket, ParametersInterface $parameters)
    {
        if ($parameters->scheme !== 'tcp') {
            return;
        }

        if (!socket_set_option($socket, SOL_TCP, TCP_NODELAY, 1)) {
            $this->emitSocketError();
        }

        if (!socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1)) {
            $this->emitSocketError();
        }

        if (isset($parameters->read_write_timeout)) {
            $rwtimeout = (float) $parameters->read_write_timeout;
            $timeoutSec = floor($rwtimeout);
            $timeoutUsec = ($rwtimeout - $timeoutSec) * 1000000;

            $timeout = array(
                'sec' => $timeoutSec,
                'usec' => $timeoutUsec,
            );

            if (!socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, $timeout)) {
                $this->emitSocketError();
            }

            if (!socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, $timeout)) {
                $this->emitSocketError();
            }
        }
    }

    /**
     * Gets the address from the connection parameters.
     *
     * @param ParametersInterface $parameters Parameters used to initialize the connection.
     *
     * @return string
     */
    protected static function getAddress(ParametersInterface $parameters)
    {
        if ($parameters->scheme === 'unix') {
            return $parameters->path;
        }

        $host = $parameters->host;

        if (ip2long($host) === false) {
            if (false === $addresses = gethostbynamel($host)) {
                return false;
            }

            return $addresses[array_rand($addresses)];
        }

        return $host;
    }

    /**
     * Opens the actual connection to the server with a timeout.
     *
     * @param ParametersInterface $parameters Parameters used to initialize the connection.
     *
     * @return string
     */
    private function connectWithTimeout(ParametersInterface $parameters)
    {
        if (false === $host = self::getAddress($parameters)) {
            $this->onConnectionError("Cannot resolve the address of '$parameters->host'.");
        }

        $socket = $this->getResource();

        socket_set_nonblock($socket);

        if (@socket_connect($socket, $host, (int) $parameters->port) === false) {
            $error = socket_last_error();

            if ($error != SOCKET_EINPROGRESS && $error != SOCKET_EALREADY) {
                $this->emitSocketError();
            }
        }

        socket_set_block($socket);

        $null = null;
        $selectable = array($socket);

        $timeout = (float) $parameters->timeout;
        $timeoutSecs = floor($timeout);
        $timeoutUSecs = ($timeout - $timeoutSecs) * 1000000;

        $selected = socket_select($selectable, $selectable, $null, $timeoutSecs, $timeoutUSecs);

        if ($selected === 2) {
            $this->onConnectionError('Connection refused.', SOCKET_ECONNREFUSED);
        }
        if ($selected === 0) {
            $this->onConnectionError('Connection timed out.', SOCKET_ETIMEDOUT);
        }
        if ($selected === false) {
            $this->emitSocketError();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function connect()
    {
        if (parent::connect()) {
            $this->connectWithTimeout($this->parameters);

            if ($this->initCommands) {
                foreach ($this->initCommands as $command) {
                    $this->executeCommand($command);
                }
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function disconnect()
    {
        if ($this->isConnected()) {
            socket_close($this->getResource());
            parent::disconnect();
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function write($buffer)
    {
        $socket = $this->getResource();

        while (($length = strlen($buffer)) > 0) {
            $written = socket_write($socket, $buffer, $length);

            if ($length === $written) {
                return;
            }

            if ($written === false) {
                $this->onConnectionError('Error while writing bytes to the server.');
            }

            $buffer = substr($buffer, $written);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function read()
    {
        $socket = $this->getResource();
        $reader = $this->reader;

        while (PHPIREDIS_READER_STATE_INCOMPLETE === $state = phpiredis_reader_get_state($reader)) {
            if (@socket_recv($socket, $buffer, 4096, 0) === false || $buffer === '') {
                $this->emitSocketError();
            }

            phpiredis_reader_feed($reader, $buffer);
        }

        if ($state === PHPIREDIS_READER_STATE_COMPLETE) {
            return phpiredis_reader_get_reply($reader);
        } else {
            $this->onProtocolError(phpiredis_reader_get_error($reader));

            return;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function writeRequest(CommandInterface $command)
    {
        $arguments = $command->getArguments();
        array_unshift($arguments, $command->getId());

        $this->write(phpiredis_format_command($arguments));
    }

    /**
     * {@inheritdoc}
     */
    public function __wakeup()
    {
        $this->assertExtensions();
        $this->reader = $this->createReader();
    }
}

/**
 * Connection abstraction to Redis servers based on PHP's stream that uses an
 * external protocol processor defining the protocol used for the communication.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class CompositeStreamConnection extends StreamConnection implements CompositeConnectionInterface
{
    protected $protocol;

    /**
     * @param ParametersInterface        $parameters Initialization parameters for the connection.
     * @param ProtocolProcessorInterface $protocol   Protocol processor.
     */
    public function __construct(
        ParametersInterface $parameters,
        ProtocolProcessorInterface $protocol = null
    ) {
        $this->parameters = $this->assertParameters($parameters);
        $this->protocol = $protocol ?: new TextProtocolProcessor();
    }

    /**
     * {@inheritdoc}
     */
    public function getProtocol()
    {
        return $this->protocol;
    }

    /**
     * {@inheritdoc}
     */
    public function writeBuffer($buffer)
    {
        $this->write($buffer);
    }

    /**
     * {@inheritdoc}
     */
    public function readBuffer($length)
    {
        if ($length <= 0) {
            throw new InvalidArgumentException('Length parameter must be greater than 0.');
        }

        $value = '';
        $socket = $this->getResource();

        do {
            $chunk = fread($socket, $length);

            if ($chunk === false || $chunk === '') {
                $this->onConnectionError('Error while reading bytes from the server.');
            }

            $value .= $chunk;
        } while (($length -= strlen($chunk)) > 0);

        return $value;
    }

    /**
     * {@inheritdoc}
     */
    public function readLine()
    {
        $value = '';
        $socket = $this->getResource();

        do {
            $chunk = fgets($socket);

            if ($chunk === false || $chunk === '') {
                $this->onConnectionError('Error while reading line from the server.');
            }

            $value .= $chunk;
        } while (substr($value, -2) !== "\r\n");

        return substr($value, 0, -2);
    }

    /**
     * {@inheritdoc}
     */
    public function writeRequest(CommandInterface $command)
    {
        $this->protocol->write($this, $command);
    }

    /**
     * {@inheritdoc}
     */
    public function read()
    {
        return $this->protocol->read($this);
    }

    /**
     * {@inheritdoc}
     */
    public function __sleep()
    {
        return array_merge(parent::__sleep(), array('protocol'));
    }
}

/**
 * Exception class that identifies connection-related errors.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ConnectionException extends CommunicationException
{
}

/**
 * Container for connection parameters used to initialize connections to Redis.
 *
 * {@inheritdoc}
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class Parameters implements ParametersInterface
{
    private $parameters;

    private static $defaults = array(
        'scheme' => 'tcp',
        'host' => '127.0.0.1',
        'port' => 6379,
        'timeout' => 5.0,
    );

    /**
     * @param array $parameters Named array of connection parameters.
     */
    public function __construct(array $parameters = array())
    {
        $this->parameters = $this->filter($parameters) + $this->getDefaults();
    }

    /**
     * Returns some default parameters with their values.
     *
     * @return array
     */
    protected function getDefaults()
    {
        return self::$defaults;
    }

    /**
     * Creates a new instance by supplying the initial parameters either in the
     * form of an URI string or a named array.
     *
     * @param array|string $parameters Set of connection parameters.
     *
     * @return Parameters
     */
    public static function create($parameters)
    {
        if (is_string($parameters)) {
            $parameters = static::parse($parameters);
        }

        return new static($parameters ?: array());
    }

    /**
     * Parses an URI string returning an array of connection parameters.
     *
     * @param string $uri URI string.
     *
     * @return array
     *
     * @throws \InvalidArgumentException
     */
    public static function parse($uri)
    {
        if (stripos($uri, 'unix') === 0) {
            // Hack to support URIs for UNIX sockets with minimal effort.
            $uri = str_ireplace('unix:///', 'unix://localhost/', $uri);
        }

        if (!($parsed = parse_url($uri)) || !isset($parsed['host'])) {
            throw new InvalidArgumentException("Invalid parameters URI: $uri");
        }

        if (isset($parsed['query'])) {
            parse_str($parsed['query'], $queryarray);
            unset($parsed['query']);

            $parsed = array_merge($parsed, $queryarray);
        }

        return $parsed;
    }

    /**
     * Validates and converts each value of the connection parameters array.
     *
     * @param array $parameters Connection parameters.
     *
     * @return array
     */
    protected function filter(array $parameters)
    {
        return $parameters ?: array();
    }

    /**
     * {@inheritdoc}
     */
    public function __get($parameter)
    {
        if (isset($this->parameters[$parameter])) {
            return $this->parameters[$parameter];
        }
    }

    /**
     * {@inheritdoc}
     */
    public function __isset($parameter)
    {
        return isset($this->parameters[$parameter]);
    }

    /**
     * {@inheritdoc}
     */
    public function toArray()
    {
        return $this->parameters;
    }

    /**
     * {@inheritdoc}
     */
    public function __sleep()
    {
        return array('parameters');
    }
}

/**
 * Standard connection factory for creating connections to Redis nodes.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class Factory implements FactoryInterface
{
    protected $schemes = array(
        'tcp'  => 'Predis\Connection\StreamConnection',
        'unix' => 'Predis\Connection\StreamConnection',
        'http' => 'Predis\Connection\WebdisConnection',
    );

    /**
     * Checks if the provided argument represents a valid connection class
     * implementing Predis\Connection\NodeConnectionInterface. Optionally,
     * callable objects are used for lazy initialization of connection objects.
     *
     * @param mixed $initializer FQN of a connection class or a callable for lazy initialization.
     *
     * @return mixed
     *
     * @throws \InvalidArgumentException
     */
    protected function checkInitializer($initializer)
    {
        if (is_callable($initializer)) {
            return $initializer;
        }

        $class = new ReflectionClass($initializer);

        if (!$class->isSubclassOf('Predis\Connection\NodeConnectionInterface')) {
            throw new InvalidArgumentException(
                'A connection initializer must be a valid connection class or a callable object.'
            );
        }

        return $initializer;
    }

    /**
     * {@inheritdoc}
     */
    public function define($scheme, $initializer)
    {
        $this->schemes[$scheme] = $this->checkInitializer($initializer);
    }

    /**
     * {@inheritdoc}
     */
    public function undefine($scheme)
    {
        unset($this->schemes[$scheme]);
    }

    /**
     * {@inheritdoc}
     */
    public function create($parameters)
    {
        if (!$parameters instanceof ParametersInterface) {
            $parameters = $this->createParameters($parameters);
        }

        $scheme = $parameters->scheme;

        if (!isset($this->schemes[$scheme])) {
            throw new InvalidArgumentException("Unknown connection scheme: '$scheme'.");
        }

        $initializer = $this->schemes[$scheme];

        if (is_callable($initializer)) {
            $connection = call_user_func($initializer, $parameters, $this);
        } else {
            $connection = new $initializer($parameters);
            $this->prepareConnection($connection);
        }

        if (!$connection instanceof NodeConnectionInterface) {
            throw new UnexpectedValueException(
                "Objects returned by connection initializers must implement ".
                "'Predis\Connection\NodeConnectionInterface'."
            );
        }

        return $connection;
    }

    /**
     * {@inheritdoc}
     */
    public function aggregate(AggregateConnectionInterface $connection, array $parameters)
    {
        foreach ($parameters as $node) {
            $connection->add($node instanceof NodeConnectionInterface ? $node : $this->create($node));
        }
    }

    /**
     * Creates a connection parameters instance from the supplied argument.
     *
     * @param mixed $parameters Original connection parameters.
     *
     * @return ParametersInterface
     */
    protected function createParameters($parameters)
    {
        return Parameters::create($parameters);
    }

    /**
     * Prepares a connection instance after its initialization.
     *
     * @param NodeConnectionInterface $connection Connection instance.
     */
    protected function prepareConnection(NodeConnectionInterface $connection)
    {
        $parameters = $connection->getParameters();

        if (isset($parameters->password)) {
            $connection->addConnectCommand(
                new RawCommand(array('AUTH', $parameters->password))
            );
        }

        if (isset($parameters->database)) {
            $connection->addConnectCommand(
                new RawCommand(array('SELECT', $parameters->database))
            );
        }
    }
}

/* --------------------------------------------------------------------------- */

namespace Predis\Profile;

use InvalidArgumentException;
use ReflectionClass;
use Predis\ClientException;
use Predis\Command\CommandInterface;
use Predis\Command\Processor\ProcessorInterface;

/**
 * A profile defines all the features and commands supported by certain versions
 * of Redis. Instances of Predis\Client should use a server profile matching the
 * version of Redis being used.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface ProfileInterface
{
    /**
     * Returns the profile version corresponding to the Redis version.
     *
     * @return string
     */
    public function getVersion();

    /**
     * Checks if the profile supports the specified command.
     *
     * @param string $commandID Command ID.
     *
     * @return bool
     */
    public function supportsCommand($commandID);

    /**
     * Checks if the profile supports the specified list of commands.
     *
     * @param array $commandIDs List of command IDs.
     *
     * @return string
     */
    public function supportsCommands(array $commandIDs);

    /**
     * Creates a new command instance.
     *
     * @param string $commandID Command ID.
     * @param array  $arguments Arguments for the command.
     *
     * @return CommandInterface
     */
    public function createCommand($commandID, array $arguments = array());
}

/**
 * Base class implementing common functionalities for Redis server profiles.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
abstract class RedisProfile implements ProfileInterface
{
    private $commands;
    private $processor;

    /**
     *
     */
    public function __construct()
    {
        $this->commands = $this->getSupportedCommands();
    }

    /**
     * Returns a map of all the commands supported by the profile and their
     * actual PHP classes.
     *
     * @return array
     */
    abstract protected function getSupportedCommands();

    /**
     * {@inheritdoc}
     */
    public function supportsCommand($commandID)
    {
        return isset($this->commands[strtoupper($commandID)]);
    }

    /**
     * {@inheritdoc}
     */
    public function supportsCommands(array $commandIDs)
    {
        foreach ($commandIDs as $commandID) {
            if (!$this->supportsCommand($commandID)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Returns the fully-qualified name of a class representing the specified
     * command ID registered in the current server profile.
     *
     * @param string $commandID Command ID.
     *
     * @return string|null
     */
    public function getCommandClass($commandID)
    {
        if (isset($this->commands[$commandID = strtoupper($commandID)])) {
            return $this->commands[$commandID];
        }
    }

    /**
     * {@inheritdoc}
     */
    public function createCommand($commandID, array $arguments = array())
    {
        $commandID = strtoupper($commandID);

        if (!isset($this->commands[$commandID])) {
            throw new ClientException("Command '$commandID' is not a registered Redis command.");
        }

        $commandClass = $this->commands[$commandID];
        $command = new $commandClass();
        $command->setArguments($arguments);

        if (isset($this->processor)) {
            $this->processor->process($command);
        }

        return $command;
    }

    /**
     * Defines a new command in the server profile.
     *
     * @param string $commandID Command ID.
     * @param string $class     Fully-qualified name of a Predis\Command\CommandInterface.
     *
     * @throws \InvalidArgumentException
     */
    public function defineCommand($commandID, $class)
    {
        $reflection = new ReflectionClass($class);

        if (!$reflection->isSubclassOf('Predis\Command\CommandInterface')) {
            throw new InvalidArgumentException("The class '$class' is not a valid command class.");
        }

        $this->commands[strtoupper($commandID)] = $class;
    }

    /**
     * {@inheritdoc}
     */
    public function setProcessor(ProcessorInterface $processor = null)
    {
        $this->processor = $processor;
    }

    /**
     * {@inheritdoc}
     */
    public function getProcessor()
    {
        return $this->processor;
    }

    /**
     * Returns the version of server profile as its string representation.
     *
     * @return string
     */
    public function __toString()
    {
        return $this->getVersion();
    }
}

/**
 * Server profile for Redis 3.0.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class RedisVersion300 extends RedisProfile
{
    /**
     * {@inheritdoc}
     */
    public function getVersion()
    {
        return '3.0';
    }

    /**
     * {@inheritdoc}
     */
    public function getSupportedCommands()
    {
        return array(
            /* ---------------- Redis 1.2 ---------------- */

            /* commands operating on the key space */
            'EXISTS'                    => 'Predis\Command\KeyExists',
            'DEL'                       => 'Predis\Command\KeyDelete',
            'TYPE'                      => 'Predis\Command\KeyType',
            'KEYS'                      => 'Predis\Command\KeyKeys',
            'RANDOMKEY'                 => 'Predis\Command\KeyRandom',
            'RENAME'                    => 'Predis\Command\KeyRename',
            'RENAMENX'                  => 'Predis\Command\KeyRenamePreserve',
            'EXPIRE'                    => 'Predis\Command\KeyExpire',
            'EXPIREAT'                  => 'Predis\Command\KeyExpireAt',
            'TTL'                       => 'Predis\Command\KeyTimeToLive',
            'MOVE'                      => 'Predis\Command\KeyMove',
            'SORT'                      => 'Predis\Command\KeySort',
            'DUMP'                      => 'Predis\Command\KeyDump',
            'RESTORE'                   => 'Predis\Command\KeyRestore',

            /* commands operating on string values */
            'SET'                       => 'Predis\Command\StringSet',
            'SETNX'                     => 'Predis\Command\StringSetPreserve',
            'MSET'                      => 'Predis\Command\StringSetMultiple',
            'MSETNX'                    => 'Predis\Command\StringSetMultiplePreserve',
            'GET'                       => 'Predis\Command\StringGet',
            'MGET'                      => 'Predis\Command\StringGetMultiple',
            'GETSET'                    => 'Predis\Command\StringGetSet',
            'INCR'                      => 'Predis\Command\StringIncrement',
            'INCRBY'                    => 'Predis\Command\StringIncrementBy',
            'DECR'                      => 'Predis\Command\StringDecrement',
            'DECRBY'                    => 'Predis\Command\StringDecrementBy',

            /* commands operating on lists */
            'RPUSH'                     => 'Predis\Command\ListPushTail',
            'LPUSH'                     => 'Predis\Command\ListPushHead',
            'LLEN'                      => 'Predis\Command\ListLength',
            'LRANGE'                    => 'Predis\Command\ListRange',
            'LTRIM'                     => 'Predis\Command\ListTrim',
            'LINDEX'                    => 'Predis\Command\ListIndex',
            'LSET'                      => 'Predis\Command\ListSet',
            'LREM'                      => 'Predis\Command\ListRemove',
            'LPOP'                      => 'Predis\Command\ListPopFirst',
            'RPOP'                      => 'Predis\Command\ListPopLast',
            'RPOPLPUSH'                 => 'Predis\Command\ListPopLastPushHead',

            /* commands operating on sets */
            'SADD'                      => 'Predis\Command\SetAdd',
            'SREM'                      => 'Predis\Command\SetRemove',
            'SPOP'                      => 'Predis\Command\SetPop',
            'SMOVE'                     => 'Predis\Command\SetMove',
            'SCARD'                     => 'Predis\Command\SetCardinality',
            'SISMEMBER'                 => 'Predis\Command\SetIsMember',
            'SINTER'                    => 'Predis\Command\SetIntersection',
            'SINTERSTORE'               => 'Predis\Command\SetIntersectionStore',
            'SUNION'                    => 'Predis\Command\SetUnion',
            'SUNIONSTORE'               => 'Predis\Command\SetUnionStore',
            'SDIFF'                     => 'Predis\Command\SetDifference',
            'SDIFFSTORE'                => 'Predis\Command\SetDifferenceStore',
            'SMEMBERS'                  => 'Predis\Command\SetMembers',
            'SRANDMEMBER'               => 'Predis\Command\SetRandomMember',

            /* commands operating on sorted sets */
            'ZADD'                      => 'Predis\Command\ZSetAdd',
            'ZINCRBY'                   => 'Predis\Command\ZSetIncrementBy',
            'ZREM'                      => 'Predis\Command\ZSetRemove',
            'ZRANGE'                    => 'Predis\Command\ZSetRange',
            'ZREVRANGE'                 => 'Predis\Command\ZSetReverseRange',
            'ZRANGEBYSCORE'             => 'Predis\Command\ZSetRangeByScore',
            'ZCARD'                     => 'Predis\Command\ZSetCardinality',
            'ZSCORE'                    => 'Predis\Command\ZSetScore',
            'ZREMRANGEBYSCORE'          => 'Predis\Command\ZSetRemoveRangeByScore',

            /* connection related commands */
            'PING'                      => 'Predis\Command\ConnectionPing',
            'AUTH'                      => 'Predis\Command\ConnectionAuth',
            'SELECT'                    => 'Predis\Command\ConnectionSelect',
            'ECHO'                      => 'Predis\Command\ConnectionEcho',
            'QUIT'                      => 'Predis\Command\ConnectionQuit',

            /* remote server control commands */
            'INFO'                      => 'Predis\Command\ServerInfoV26x',
            'SLAVEOF'                   => 'Predis\Command\ServerSlaveOf',
            'MONITOR'                   => 'Predis\Command\ServerMonitor',
            'DBSIZE'                    => 'Predis\Command\ServerDatabaseSize',
            'FLUSHDB'                   => 'Predis\Command\ServerFlushDatabase',
            'FLUSHALL'                  => 'Predis\Command\ServerFlushAll',
            'SAVE'                      => 'Predis\Command\ServerSave',
            'BGSAVE'                    => 'Predis\Command\ServerBackgroundSave',
            'LASTSAVE'                  => 'Predis\Command\ServerLastSave',
            'SHUTDOWN'                  => 'Predis\Command\ServerShutdown',
            'BGREWRITEAOF'              => 'Predis\Command\ServerBackgroundRewriteAOF',

            /* ---------------- Redis 2.0 ---------------- */

            /* commands operating on string values */
            'SETEX'                     => 'Predis\Command\StringSetExpire',
            'APPEND'                    => 'Predis\Command\StringAppend',
            'SUBSTR'                    => 'Predis\Command\StringSubstr',

            /* commands operating on lists */
            'BLPOP'                     => 'Predis\Command\ListPopFirstBlocking',
            'BRPOP'                     => 'Predis\Command\ListPopLastBlocking',

            /* commands operating on sorted sets */
            'ZUNIONSTORE'               => 'Predis\Command\ZSetUnionStore',
            'ZINTERSTORE'               => 'Predis\Command\ZSetIntersectionStore',
            'ZCOUNT'                    => 'Predis\Command\ZSetCount',
            'ZRANK'                     => 'Predis\Command\ZSetRank',
            'ZREVRANK'                  => 'Predis\Command\ZSetReverseRank',
            'ZREMRANGEBYRANK'           => 'Predis\Command\ZSetRemoveRangeByRank',

            /* commands operating on hashes */
            'HSET'                      => 'Predis\Command\HashSet',
            'HSETNX'                    => 'Predis\Command\HashSetPreserve',
            'HMSET'                     => 'Predis\Command\HashSetMultiple',
            'HINCRBY'                   => 'Predis\Command\HashIncrementBy',
            'HGET'                      => 'Predis\Command\HashGet',
            'HMGET'                     => 'Predis\Command\HashGetMultiple',
            'HDEL'                      => 'Predis\Command\HashDelete',
            'HEXISTS'                   => 'Predis\Command\HashExists',
            'HLEN'                      => 'Predis\Command\HashLength',
            'HKEYS'                     => 'Predis\Command\HashKeys',
            'HVALS'                     => 'Predis\Command\HashValues',
            'HGETALL'                   => 'Predis\Command\HashGetAll',

            /* transactions */
            'MULTI'                     => 'Predis\Command\TransactionMulti',
            'EXEC'                      => 'Predis\Command\TransactionExec',
            'DISCARD'                   => 'Predis\Command\TransactionDiscard',

            /* publish - subscribe */
            'SUBSCRIBE'                 => 'Predis\Command\PubSubSubscribe',
            'UNSUBSCRIBE'               => 'Predis\Command\PubSubUnsubscribe',
            'PSUBSCRIBE'                => 'Predis\Command\PubSubSubscribeByPattern',
            'PUNSUBSCRIBE'              => 'Predis\Command\PubSubUnsubscribeByPattern',
            'PUBLISH'                   => 'Predis\Command\PubSubPublish',

            /* remote server control commands */
            'CONFIG'                    => 'Predis\Command\ServerConfig',

            /* ---------------- Redis 2.2 ---------------- */

            /* commands operating on the key space */
            'PERSIST'                   => 'Predis\Command\KeyPersist',

            /* commands operating on string values */
            'STRLEN'                    => 'Predis\Command\StringStrlen',
            'SETRANGE'                  => 'Predis\Command\StringSetRange',
            'GETRANGE'                  => 'Predis\Command\StringGetRange',
            'SETBIT'                    => 'Predis\Command\StringSetBit',
            'GETBIT'                    => 'Predis\Command\StringGetBit',

            /* commands operating on lists */
            'RPUSHX'                    => 'Predis\Command\ListPushTailX',
            'LPUSHX'                    => 'Predis\Command\ListPushHeadX',
            'LINSERT'                   => 'Predis\Command\ListInsert',
            'BRPOPLPUSH'                => 'Predis\Command\ListPopLastPushHeadBlocking',

            /* commands operating on sorted sets */
            'ZREVRANGEBYSCORE'          => 'Predis\Command\ZSetReverseRangeByScore',

            /* transactions */
            'WATCH'                     => 'Predis\Command\TransactionWatch',
            'UNWATCH'                   => 'Predis\Command\TransactionUnwatch',

            /* remote server control commands */
            'OBJECT'                    => 'Predis\Command\ServerObject',
            'SLOWLOG'                   => 'Predis\Command\ServerSlowlog',

            /* ---------------- Redis 2.4 ---------------- */

            /* remote server control commands */
            'CLIENT'                    => 'Predis\Command\ServerClient',

            /* ---------------- Redis 2.6 ---------------- */

            /* commands operating on the key space */
            'PTTL'                      => 'Predis\Command\KeyPreciseTimeToLive',
            'PEXPIRE'                   => 'Predis\Command\KeyPreciseExpire',
            'PEXPIREAT'                 => 'Predis\Command\KeyPreciseExpireAt',

            /* commands operating on string values */
            'PSETEX'                    => 'Predis\Command\StringPreciseSetExpire',
            'INCRBYFLOAT'               => 'Predis\Command\StringIncrementByFloat',
            'BITOP'                     => 'Predis\Command\StringBitOp',
            'BITCOUNT'                  => 'Predis\Command\StringBitCount',

            /* commands operating on hashes */
            'HINCRBYFLOAT'              => 'Predis\Command\HashIncrementByFloat',

            /* scripting */
            'EVAL'                      => 'Predis\Command\ServerEval',
            'EVALSHA'                   => 'Predis\Command\ServerEvalSHA',
            'SCRIPT'                    => 'Predis\Command\ServerScript',

            /* remote server control commands */
            'TIME'                      => 'Predis\Command\ServerTime',
            'SENTINEL'                  => 'Predis\Command\ServerSentinel',

            /* ---------------- Redis 2.8 ---------------- */

            /* commands operating on the key space */
            'SCAN'                      => 'Predis\Command\KeyScan',

            /* commands operating on string values */
            'BITPOS'                    => 'Predis\Command\StringBitPos',

            /* commands operating on sets */
            'SSCAN'                     => 'Predis\Command\SetScan',

            /* commands operating on sorted sets */
            'ZSCAN'                     => 'Predis\Command\ZSetScan',
            'ZLEXCOUNT'                 => 'Predis\Command\ZSetLexCount',
            'ZRANGEBYLEX'               => 'Predis\Command\ZSetRangeByLex',
            'ZREMRANGEBYLEX'            => 'Predis\Command\ZSetRemoveRangeByLex',

            /* commands operating on hashes */
            'HSCAN'                     => 'Predis\Command\HashScan',

            /* publish - subscribe */
            'PUBSUB'                    => 'Predis\Command\PubSubPubsub',

            /* commands operating on HyperLogLog */
            'PFADD'                     => 'Predis\Command\HyperLogLogAdd',
            'PFCOUNT'                   => 'Predis\Command\HyperLogLogCount',
            'PFMERGE'                   => 'Predis\Command\HyperLogLogMerge',

            /* remote server control commands */
            'COMMAND'                   => 'Predis\Command\ServerCommand',

            /* ---------------- Redis 3.0 ---------------- */

        );
    }
}

/**
 * Server profile for Redis 2.6.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class RedisVersion260 extends RedisProfile
{
    /**
     * {@inheritdoc}
     */
    public function getVersion()
    {
        return '2.6';
    }

    /**
     * {@inheritdoc}
     */
    public function getSupportedCommands()
    {
        return array(
            /* ---------------- Redis 1.2 ---------------- */

            /* commands operating on the key space */
            'EXISTS'                    => 'Predis\Command\KeyExists',
            'DEL'                       => 'Predis\Command\KeyDelete',
            'TYPE'                      => 'Predis\Command\KeyType',
            'KEYS'                      => 'Predis\Command\KeyKeys',
            'RANDOMKEY'                 => 'Predis\Command\KeyRandom',
            'RENAME'                    => 'Predis\Command\KeyRename',
            'RENAMENX'                  => 'Predis\Command\KeyRenamePreserve',
            'EXPIRE'                    => 'Predis\Command\KeyExpire',
            'EXPIREAT'                  => 'Predis\Command\KeyExpireAt',
            'TTL'                       => 'Predis\Command\KeyTimeToLive',
            'MOVE'                      => 'Predis\Command\KeyMove',
            'SORT'                      => 'Predis\Command\KeySort',
            'DUMP'                      => 'Predis\Command\KeyDump',
            'RESTORE'                   => 'Predis\Command\KeyRestore',

            /* commands operating on string values */
            'SET'                       => 'Predis\Command\StringSet',
            'SETNX'                     => 'Predis\Command\StringSetPreserve',
            'MSET'                      => 'Predis\Command\StringSetMultiple',
            'MSETNX'                    => 'Predis\Command\StringSetMultiplePreserve',
            'GET'                       => 'Predis\Command\StringGet',
            'MGET'                      => 'Predis\Command\StringGetMultiple',
            'GETSET'                    => 'Predis\Command\StringGetSet',
            'INCR'                      => 'Predis\Command\StringIncrement',
            'INCRBY'                    => 'Predis\Command\StringIncrementBy',
            'DECR'                      => 'Predis\Command\StringDecrement',
            'DECRBY'                    => 'Predis\Command\StringDecrementBy',

            /* commands operating on lists */
            'RPUSH'                     => 'Predis\Command\ListPushTail',
            'LPUSH'                     => 'Predis\Command\ListPushHead',
            'LLEN'                      => 'Predis\Command\ListLength',
            'LRANGE'                    => 'Predis\Command\ListRange',
            'LTRIM'                     => 'Predis\Command\ListTrim',
            'LINDEX'                    => 'Predis\Command\ListIndex',
            'LSET'                      => 'Predis\Command\ListSet',
            'LREM'                      => 'Predis\Command\ListRemove',
            'LPOP'                      => 'Predis\Command\ListPopFirst',
            'RPOP'                      => 'Predis\Command\ListPopLast',
            'RPOPLPUSH'                 => 'Predis\Command\ListPopLastPushHead',

            /* commands operating on sets */
            'SADD'                      => 'Predis\Command\SetAdd',
            'SREM'                      => 'Predis\Command\SetRemove',
            'SPOP'                      => 'Predis\Command\SetPop',
            'SMOVE'                     => 'Predis\Command\SetMove',
            'SCARD'                     => 'Predis\Command\SetCardinality',
            'SISMEMBER'                 => 'Predis\Command\SetIsMember',
            'SINTER'                    => 'Predis\Command\SetIntersection',
            'SINTERSTORE'               => 'Predis\Command\SetIntersectionStore',
            'SUNION'                    => 'Predis\Command\SetUnion',
            'SUNIONSTORE'               => 'Predis\Command\SetUnionStore',
            'SDIFF'                     => 'Predis\Command\SetDifference',
            'SDIFFSTORE'                => 'Predis\Command\SetDifferenceStore',
            'SMEMBERS'                  => 'Predis\Command\SetMembers',
            'SRANDMEMBER'               => 'Predis\Command\SetRandomMember',

            /* commands operating on sorted sets */
            'ZADD'                      => 'Predis\Command\ZSetAdd',
            'ZINCRBY'                   => 'Predis\Command\ZSetIncrementBy',
            'ZREM'                      => 'Predis\Command\ZSetRemove',
            'ZRANGE'                    => 'Predis\Command\ZSetRange',
            'ZREVRANGE'                 => 'Predis\Command\ZSetReverseRange',
            'ZRANGEBYSCORE'             => 'Predis\Command\ZSetRangeByScore',
            'ZCARD'                     => 'Predis\Command\ZSetCardinality',
            'ZSCORE'                    => 'Predis\Command\ZSetScore',
            'ZREMRANGEBYSCORE'          => 'Predis\Command\ZSetRemoveRangeByScore',

            /* connection related commands */
            'PING'                      => 'Predis\Command\ConnectionPing',
            'AUTH'                      => 'Predis\Command\ConnectionAuth',
            'SELECT'                    => 'Predis\Command\ConnectionSelect',
            'ECHO'                      => 'Predis\Command\ConnectionEcho',
            'QUIT'                      => 'Predis\Command\ConnectionQuit',

            /* remote server control commands */
            'INFO'                      => 'Predis\Command\ServerInfoV26x',
            'SLAVEOF'                   => 'Predis\Command\ServerSlaveOf',
            'MONITOR'                   => 'Predis\Command\ServerMonitor',
            'DBSIZE'                    => 'Predis\Command\ServerDatabaseSize',
            'FLUSHDB'                   => 'Predis\Command\ServerFlushDatabase',
            'FLUSHALL'                  => 'Predis\Command\ServerFlushAll',
            'SAVE'                      => 'Predis\Command\ServerSave',
            'BGSAVE'                    => 'Predis\Command\ServerBackgroundSave',
            'LASTSAVE'                  => 'Predis\Command\ServerLastSave',
            'SHUTDOWN'                  => 'Predis\Command\ServerShutdown',
            'BGREWRITEAOF'              => 'Predis\Command\ServerBackgroundRewriteAOF',

            /* ---------------- Redis 2.0 ---------------- */

            /* commands operating on string values */
            'SETEX'                     => 'Predis\Command\StringSetExpire',
            'APPEND'                    => 'Predis\Command\StringAppend',
            'SUBSTR'                    => 'Predis\Command\StringSubstr',

            /* commands operating on lists */
            'BLPOP'                     => 'Predis\Command\ListPopFirstBlocking',
            'BRPOP'                     => 'Predis\Command\ListPopLastBlocking',

            /* commands operating on sorted sets */
            'ZUNIONSTORE'               => 'Predis\Command\ZSetUnionStore',
            'ZINTERSTORE'               => 'Predis\Command\ZSetIntersectionStore',
            'ZCOUNT'                    => 'Predis\Command\ZSetCount',
            'ZRANK'                     => 'Predis\Command\ZSetRank',
            'ZREVRANK'                  => 'Predis\Command\ZSetReverseRank',
            'ZREMRANGEBYRANK'           => 'Predis\Command\ZSetRemoveRangeByRank',

            /* commands operating on hashes */
            'HSET'                      => 'Predis\Command\HashSet',
            'HSETNX'                    => 'Predis\Command\HashSetPreserve',
            'HMSET'                     => 'Predis\Command\HashSetMultiple',
            'HINCRBY'                   => 'Predis\Command\HashIncrementBy',
            'HGET'                      => 'Predis\Command\HashGet',
            'HMGET'                     => 'Predis\Command\HashGetMultiple',
            'HDEL'                      => 'Predis\Command\HashDelete',
            'HEXISTS'                   => 'Predis\Command\HashExists',
            'HLEN'                      => 'Predis\Command\HashLength',
            'HKEYS'                     => 'Predis\Command\HashKeys',
            'HVALS'                     => 'Predis\Command\HashValues',
            'HGETALL'                   => 'Predis\Command\HashGetAll',

            /* transactions */
            'MULTI'                     => 'Predis\Command\TransactionMulti',
            'EXEC'                      => 'Predis\Command\TransactionExec',
            'DISCARD'                   => 'Predis\Command\TransactionDiscard',

            /* publish - subscribe */
            'SUBSCRIBE'                 => 'Predis\Command\PubSubSubscribe',
            'UNSUBSCRIBE'               => 'Predis\Command\PubSubUnsubscribe',
            'PSUBSCRIBE'                => 'Predis\Command\PubSubSubscribeByPattern',
            'PUNSUBSCRIBE'              => 'Predis\Command\PubSubUnsubscribeByPattern',
            'PUBLISH'                   => 'Predis\Command\PubSubPublish',

            /* remote server control commands */
            'CONFIG'                    => 'Predis\Command\ServerConfig',

            /* ---------------- Redis 2.2 ---------------- */

            /* commands operating on the key space */
            'PERSIST'                   => 'Predis\Command\KeyPersist',

            /* commands operating on string values */
            'STRLEN'                    => 'Predis\Command\StringStrlen',
            'SETRANGE'                  => 'Predis\Command\StringSetRange',
            'GETRANGE'                  => 'Predis\Command\StringGetRange',
            'SETBIT'                    => 'Predis\Command\StringSetBit',
            'GETBIT'                    => 'Predis\Command\StringGetBit',

            /* commands operating on lists */
            'RPUSHX'                    => 'Predis\Command\ListPushTailX',
            'LPUSHX'                    => 'Predis\Command\ListPushHeadX',
            'LINSERT'                   => 'Predis\Command\ListInsert',
            'BRPOPLPUSH'                => 'Predis\Command\ListPopLastPushHeadBlocking',

            /* commands operating on sorted sets */
            'ZREVRANGEBYSCORE'          => 'Predis\Command\ZSetReverseRangeByScore',

            /* transactions */
            'WATCH'                     => 'Predis\Command\TransactionWatch',
            'UNWATCH'                   => 'Predis\Command\TransactionUnwatch',

            /* remote server control commands */
            'OBJECT'                    => 'Predis\Command\ServerObject',
            'SLOWLOG'                   => 'Predis\Command\ServerSlowlog',

            /* ---------------- Redis 2.4 ---------------- */

            /* remote server control commands */
            'CLIENT'                    => 'Predis\Command\ServerClient',

            /* ---------------- Redis 2.6 ---------------- */

            /* commands operating on the key space */
            'PTTL'                      => 'Predis\Command\KeyPreciseTimeToLive',
            'PEXPIRE'                   => 'Predis\Command\KeyPreciseExpire',
            'PEXPIREAT'                 => 'Predis\Command\KeyPreciseExpireAt',

            /* commands operating on string values */
            'PSETEX'                    => 'Predis\Command\StringPreciseSetExpire',
            'INCRBYFLOAT'               => 'Predis\Command\StringIncrementByFloat',
            'BITOP'                     => 'Predis\Command\StringBitOp',
            'BITCOUNT'                  => 'Predis\Command\StringBitCount',

            /* commands operating on hashes */
            'HINCRBYFLOAT'              => 'Predis\Command\HashIncrementByFloat',

            /* scripting */
            'EVAL'                      => 'Predis\Command\ServerEval',
            'EVALSHA'                   => 'Predis\Command\ServerEvalSHA',
            'SCRIPT'                    => 'Predis\Command\ServerScript',

            /* remote server control commands */
            'TIME'                      => 'Predis\Command\ServerTime',
            'SENTINEL'                  => 'Predis\Command\ServerSentinel',
        );
    }
}

/**
 * Server profile for Redis 2.8.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class RedisVersion280 extends RedisProfile
{
    /**
     * {@inheritdoc}
     */
    public function getVersion()
    {
        return '2.8';
    }

    /**
     * {@inheritdoc}
     */
    public function getSupportedCommands()
    {
        return array(
            /* ---------------- Redis 1.2 ---------------- */

            /* commands operating on the key space */
            'EXISTS'                    => 'Predis\Command\KeyExists',
            'DEL'                       => 'Predis\Command\KeyDelete',
            'TYPE'                      => 'Predis\Command\KeyType',
            'KEYS'                      => 'Predis\Command\KeyKeys',
            'RANDOMKEY'                 => 'Predis\Command\KeyRandom',
            'RENAME'                    => 'Predis\Command\KeyRename',
            'RENAMENX'                  => 'Predis\Command\KeyRenamePreserve',
            'EXPIRE'                    => 'Predis\Command\KeyExpire',
            'EXPIREAT'                  => 'Predis\Command\KeyExpireAt',
            'TTL'                       => 'Predis\Command\KeyTimeToLive',
            'MOVE'                      => 'Predis\Command\KeyMove',
            'SORT'                      => 'Predis\Command\KeySort',
            'DUMP'                      => 'Predis\Command\KeyDump',
            'RESTORE'                   => 'Predis\Command\KeyRestore',

            /* commands operating on string values */
            'SET'                       => 'Predis\Command\StringSet',
            'SETNX'                     => 'Predis\Command\StringSetPreserve',
            'MSET'                      => 'Predis\Command\StringSetMultiple',
            'MSETNX'                    => 'Predis\Command\StringSetMultiplePreserve',
            'GET'                       => 'Predis\Command\StringGet',
            'MGET'                      => 'Predis\Command\StringGetMultiple',
            'GETSET'                    => 'Predis\Command\StringGetSet',
            'INCR'                      => 'Predis\Command\StringIncrement',
            'INCRBY'                    => 'Predis\Command\StringIncrementBy',
            'DECR'                      => 'Predis\Command\StringDecrement',
            'DECRBY'                    => 'Predis\Command\StringDecrementBy',

            /* commands operating on lists */
            'RPUSH'                     => 'Predis\Command\ListPushTail',
            'LPUSH'                     => 'Predis\Command\ListPushHead',
            'LLEN'                      => 'Predis\Command\ListLength',
            'LRANGE'                    => 'Predis\Command\ListRange',
            'LTRIM'                     => 'Predis\Command\ListTrim',
            'LINDEX'                    => 'Predis\Command\ListIndex',
            'LSET'                      => 'Predis\Command\ListSet',
            'LREM'                      => 'Predis\Command\ListRemove',
            'LPOP'                      => 'Predis\Command\ListPopFirst',
            'RPOP'                      => 'Predis\Command\ListPopLast',
            'RPOPLPUSH'                 => 'Predis\Command\ListPopLastPushHead',

            /* commands operating on sets */
            'SADD'                      => 'Predis\Command\SetAdd',
            'SREM'                      => 'Predis\Command\SetRemove',
            'SPOP'                      => 'Predis\Command\SetPop',
            'SMOVE'                     => 'Predis\Command\SetMove',
            'SCARD'                     => 'Predis\Command\SetCardinality',
            'SISMEMBER'                 => 'Predis\Command\SetIsMember',
            'SINTER'                    => 'Predis\Command\SetIntersection',
            'SINTERSTORE'               => 'Predis\Command\SetIntersectionStore',
            'SUNION'                    => 'Predis\Command\SetUnion',
            'SUNIONSTORE'               => 'Predis\Command\SetUnionStore',
            'SDIFF'                     => 'Predis\Command\SetDifference',
            'SDIFFSTORE'                => 'Predis\Command\SetDifferenceStore',
            'SMEMBERS'                  => 'Predis\Command\SetMembers',
            'SRANDMEMBER'               => 'Predis\Command\SetRandomMember',

            /* commands operating on sorted sets */
            'ZADD'                      => 'Predis\Command\ZSetAdd',
            'ZINCRBY'                   => 'Predis\Command\ZSetIncrementBy',
            'ZREM'                      => 'Predis\Command\ZSetRemove',
            'ZRANGE'                    => 'Predis\Command\ZSetRange',
            'ZREVRANGE'                 => 'Predis\Command\ZSetReverseRange',
            'ZRANGEBYSCORE'             => 'Predis\Command\ZSetRangeByScore',
            'ZCARD'                     => 'Predis\Command\ZSetCardinality',
            'ZSCORE'                    => 'Predis\Command\ZSetScore',
            'ZREMRANGEBYSCORE'          => 'Predis\Command\ZSetRemoveRangeByScore',

            /* connection related commands */
            'PING'                      => 'Predis\Command\ConnectionPing',
            'AUTH'                      => 'Predis\Command\ConnectionAuth',
            'SELECT'                    => 'Predis\Command\ConnectionSelect',
            'ECHO'                      => 'Predis\Command\ConnectionEcho',
            'QUIT'                      => 'Predis\Command\ConnectionQuit',

            /* remote server control commands */
            'INFO'                      => 'Predis\Command\ServerInfoV26x',
            'SLAVEOF'                   => 'Predis\Command\ServerSlaveOf',
            'MONITOR'                   => 'Predis\Command\ServerMonitor',
            'DBSIZE'                    => 'Predis\Command\ServerDatabaseSize',
            'FLUSHDB'                   => 'Predis\Command\ServerFlushDatabase',
            'FLUSHALL'                  => 'Predis\Command\ServerFlushAll',
            'SAVE'                      => 'Predis\Command\ServerSave',
            'BGSAVE'                    => 'Predis\Command\ServerBackgroundSave',
            'LASTSAVE'                  => 'Predis\Command\ServerLastSave',
            'SHUTDOWN'                  => 'Predis\Command\ServerShutdown',
            'BGREWRITEAOF'              => 'Predis\Command\ServerBackgroundRewriteAOF',

            /* ---------------- Redis 2.0 ---------------- */

            /* commands operating on string values */
            'SETEX'                     => 'Predis\Command\StringSetExpire',
            'APPEND'                    => 'Predis\Command\StringAppend',
            'SUBSTR'                    => 'Predis\Command\StringSubstr',

            /* commands operating on lists */
            'BLPOP'                     => 'Predis\Command\ListPopFirstBlocking',
            'BRPOP'                     => 'Predis\Command\ListPopLastBlocking',

            /* commands operating on sorted sets */
            'ZUNIONSTORE'               => 'Predis\Command\ZSetUnionStore',
            'ZINTERSTORE'               => 'Predis\Command\ZSetIntersectionStore',
            'ZCOUNT'                    => 'Predis\Command\ZSetCount',
            'ZRANK'                     => 'Predis\Command\ZSetRank',
            'ZREVRANK'                  => 'Predis\Command\ZSetReverseRank',
            'ZREMRANGEBYRANK'           => 'Predis\Command\ZSetRemoveRangeByRank',

            /* commands operating on hashes */
            'HSET'                      => 'Predis\Command\HashSet',
            'HSETNX'                    => 'Predis\Command\HashSetPreserve',
            'HMSET'                     => 'Predis\Command\HashSetMultiple',
            'HINCRBY'                   => 'Predis\Command\HashIncrementBy',
            'HGET'                      => 'Predis\Command\HashGet',
            'HMGET'                     => 'Predis\Command\HashGetMultiple',
            'HDEL'                      => 'Predis\Command\HashDelete',
            'HEXISTS'                   => 'Predis\Command\HashExists',
            'HLEN'                      => 'Predis\Command\HashLength',
            'HKEYS'                     => 'Predis\Command\HashKeys',
            'HVALS'                     => 'Predis\Command\HashValues',
            'HGETALL'                   => 'Predis\Command\HashGetAll',

            /* transactions */
            'MULTI'                     => 'Predis\Command\TransactionMulti',
            'EXEC'                      => 'Predis\Command\TransactionExec',
            'DISCARD'                   => 'Predis\Command\TransactionDiscard',

            /* publish - subscribe */
            'SUBSCRIBE'                 => 'Predis\Command\PubSubSubscribe',
            'UNSUBSCRIBE'               => 'Predis\Command\PubSubUnsubscribe',
            'PSUBSCRIBE'                => 'Predis\Command\PubSubSubscribeByPattern',
            'PUNSUBSCRIBE'              => 'Predis\Command\PubSubUnsubscribeByPattern',
            'PUBLISH'                   => 'Predis\Command\PubSubPublish',

            /* remote server control commands */
            'CONFIG'                    => 'Predis\Command\ServerConfig',

            /* ---------------- Redis 2.2 ---------------- */

            /* commands operating on the key space */
            'PERSIST'                   => 'Predis\Command\KeyPersist',

            /* commands operating on string values */
            'STRLEN'                    => 'Predis\Command\StringStrlen',
            'SETRANGE'                  => 'Predis\Command\StringSetRange',
            'GETRANGE'                  => 'Predis\Command\StringGetRange',
            'SETBIT'                    => 'Predis\Command\StringSetBit',
            'GETBIT'                    => 'Predis\Command\StringGetBit',

            /* commands operating on lists */
            'RPUSHX'                    => 'Predis\Command\ListPushTailX',
            'LPUSHX'                    => 'Predis\Command\ListPushHeadX',
            'LINSERT'                   => 'Predis\Command\ListInsert',
            'BRPOPLPUSH'                => 'Predis\Command\ListPopLastPushHeadBlocking',

            /* commands operating on sorted sets */
            'ZREVRANGEBYSCORE'          => 'Predis\Command\ZSetReverseRangeByScore',

            /* transactions */
            'WATCH'                     => 'Predis\Command\TransactionWatch',
            'UNWATCH'                   => 'Predis\Command\TransactionUnwatch',

            /* remote server control commands */
            'OBJECT'                    => 'Predis\Command\ServerObject',
            'SLOWLOG'                   => 'Predis\Command\ServerSlowlog',

            /* ---------------- Redis 2.4 ---------------- */

            /* remote server control commands */
            'CLIENT'                    => 'Predis\Command\ServerClient',

            /* ---------------- Redis 2.6 ---------------- */

            /* commands operating on the key space */
            'PTTL'                      => 'Predis\Command\KeyPreciseTimeToLive',
            'PEXPIRE'                   => 'Predis\Command\KeyPreciseExpire',
            'PEXPIREAT'                 => 'Predis\Command\KeyPreciseExpireAt',

            /* commands operating on string values */
            'PSETEX'                    => 'Predis\Command\StringPreciseSetExpire',
            'INCRBYFLOAT'               => 'Predis\Command\StringIncrementByFloat',
            'BITOP'                     => 'Predis\Command\StringBitOp',
            'BITCOUNT'                  => 'Predis\Command\StringBitCount',

            /* commands operating on hashes */
            'HINCRBYFLOAT'              => 'Predis\Command\HashIncrementByFloat',

            /* scripting */
            'EVAL'                      => 'Predis\Command\ServerEval',
            'EVALSHA'                   => 'Predis\Command\ServerEvalSHA',
            'SCRIPT'                    => 'Predis\Command\ServerScript',

            /* remote server control commands */
            'TIME'                      => 'Predis\Command\ServerTime',
            'SENTINEL'                  => 'Predis\Command\ServerSentinel',

            /* ---------------- Redis 2.8 ---------------- */

            /* commands operating on the key space */
            'SCAN'                      => 'Predis\Command\KeyScan',

            /* commands operating on string values */
            'BITPOS'                    => 'Predis\Command\StringBitPos',

            /* commands operating on sets */
            'SSCAN'                     => 'Predis\Command\SetScan',

            /* commands operating on sorted sets */
            'ZSCAN'                     => 'Predis\Command\ZSetScan',
            'ZLEXCOUNT'                 => 'Predis\Command\ZSetLexCount',
            'ZRANGEBYLEX'               => 'Predis\Command\ZSetRangeByLex',
            'ZREMRANGEBYLEX'            => 'Predis\Command\ZSetRemoveRangeByLex',

            /* commands operating on hashes */
            'HSCAN'                     => 'Predis\Command\HashScan',

            /* publish - subscribe */
            'PUBSUB'                    => 'Predis\Command\PubSubPubsub',

            /* commands operating on HyperLogLog */
            'PFADD'                     => 'Predis\Command\HyperLogLogAdd',
            'PFCOUNT'                   => 'Predis\Command\HyperLogLogCount',
            'PFMERGE'                   => 'Predis\Command\HyperLogLogMerge',

            /* remote server control commands */
            'COMMAND'                   => 'Predis\Command\ServerCommand',
        );
    }
}

/**
 * Server profile for Redis 2.4.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class RedisVersion240 extends RedisProfile
{
    /**
     * {@inheritdoc}
     */
    public function getVersion()
    {
        return '2.4';
    }

    /**
     * {@inheritdoc}
     */
    public function getSupportedCommands()
    {
        return array(
            /* ---------------- Redis 1.2 ---------------- */

            /* commands operating on the key space */
            'EXISTS'                    => 'Predis\Command\KeyExists',
            'DEL'                       => 'Predis\Command\KeyDelete',
            'TYPE'                      => 'Predis\Command\KeyType',
            'KEYS'                      => 'Predis\Command\KeyKeys',
            'RANDOMKEY'                 => 'Predis\Command\KeyRandom',
            'RENAME'                    => 'Predis\Command\KeyRename',
            'RENAMENX'                  => 'Predis\Command\KeyRenamePreserve',
            'EXPIRE'                    => 'Predis\Command\KeyExpire',
            'EXPIREAT'                  => 'Predis\Command\KeyExpireAt',
            'TTL'                       => 'Predis\Command\KeyTimeToLive',
            'MOVE'                      => 'Predis\Command\KeyMove',
            'SORT'                      => 'Predis\Command\KeySort',

            /* commands operating on string values */
            'SET'                       => 'Predis\Command\StringSet',
            'SETNX'                     => 'Predis\Command\StringSetPreserve',
            'MSET'                      => 'Predis\Command\StringSetMultiple',
            'MSETNX'                    => 'Predis\Command\StringSetMultiplePreserve',
            'GET'                       => 'Predis\Command\StringGet',
            'MGET'                      => 'Predis\Command\StringGetMultiple',
            'GETSET'                    => 'Predis\Command\StringGetSet',
            'INCR'                      => 'Predis\Command\StringIncrement',
            'INCRBY'                    => 'Predis\Command\StringIncrementBy',
            'DECR'                      => 'Predis\Command\StringDecrement',
            'DECRBY'                    => 'Predis\Command\StringDecrementBy',

            /* commands operating on lists */
            'RPUSH'                     => 'Predis\Command\ListPushTail',
            'LPUSH'                     => 'Predis\Command\ListPushHead',
            'LLEN'                      => 'Predis\Command\ListLength',
            'LRANGE'                    => 'Predis\Command\ListRange',
            'LTRIM'                     => 'Predis\Command\ListTrim',
            'LINDEX'                    => 'Predis\Command\ListIndex',
            'LSET'                      => 'Predis\Command\ListSet',
            'LREM'                      => 'Predis\Command\ListRemove',
            'LPOP'                      => 'Predis\Command\ListPopFirst',
            'RPOP'                      => 'Predis\Command\ListPopLast',
            'RPOPLPUSH'                 => 'Predis\Command\ListPopLastPushHead',

            /* commands operating on sets */
            'SADD'                      => 'Predis\Command\SetAdd',
            'SREM'                      => 'Predis\Command\SetRemove',
            'SPOP'                      => 'Predis\Command\SetPop',
            'SMOVE'                     => 'Predis\Command\SetMove',
            'SCARD'                     => 'Predis\Command\SetCardinality',
            'SISMEMBER'                 => 'Predis\Command\SetIsMember',
            'SINTER'                    => 'Predis\Command\SetIntersection',
            'SINTERSTORE'               => 'Predis\Command\SetIntersectionStore',
            'SUNION'                    => 'Predis\Command\SetUnion',
            'SUNIONSTORE'               => 'Predis\Command\SetUnionStore',
            'SDIFF'                     => 'Predis\Command\SetDifference',
            'SDIFFSTORE'                => 'Predis\Command\SetDifferenceStore',
            'SMEMBERS'                  => 'Predis\Command\SetMembers',
            'SRANDMEMBER'               => 'Predis\Command\SetRandomMember',

            /* commands operating on sorted sets */
            'ZADD'                      => 'Predis\Command\ZSetAdd',
            'ZINCRBY'                   => 'Predis\Command\ZSetIncrementBy',
            'ZREM'                      => 'Predis\Command\ZSetRemove',
            'ZRANGE'                    => 'Predis\Command\ZSetRange',
            'ZREVRANGE'                 => 'Predis\Command\ZSetReverseRange',
            'ZRANGEBYSCORE'             => 'Predis\Command\ZSetRangeByScore',
            'ZCARD'                     => 'Predis\Command\ZSetCardinality',
            'ZSCORE'                    => 'Predis\Command\ZSetScore',
            'ZREMRANGEBYSCORE'          => 'Predis\Command\ZSetRemoveRangeByScore',

            /* connection related commands */
            'PING'                      => 'Predis\Command\ConnectionPing',
            'AUTH'                      => 'Predis\Command\ConnectionAuth',
            'SELECT'                    => 'Predis\Command\ConnectionSelect',
            'ECHO'                      => 'Predis\Command\ConnectionEcho',
            'QUIT'                      => 'Predis\Command\ConnectionQuit',

            /* remote server control commands */
            'INFO'                      => 'Predis\Command\ServerInfo',
            'SLAVEOF'                   => 'Predis\Command\ServerSlaveOf',
            'MONITOR'                   => 'Predis\Command\ServerMonitor',
            'DBSIZE'                    => 'Predis\Command\ServerDatabaseSize',
            'FLUSHDB'                   => 'Predis\Command\ServerFlushDatabase',
            'FLUSHALL'                  => 'Predis\Command\ServerFlushAll',
            'SAVE'                      => 'Predis\Command\ServerSave',
            'BGSAVE'                    => 'Predis\Command\ServerBackgroundSave',
            'LASTSAVE'                  => 'Predis\Command\ServerLastSave',
            'SHUTDOWN'                  => 'Predis\Command\ServerShutdown',
            'BGREWRITEAOF'              => 'Predis\Command\ServerBackgroundRewriteAOF',

            /* ---------------- Redis 2.0 ---------------- */

            /* commands operating on string values */
            'SETEX'                     => 'Predis\Command\StringSetExpire',
            'APPEND'                    => 'Predis\Command\StringAppend',
            'SUBSTR'                    => 'Predis\Command\StringSubstr',

            /* commands operating on lists */
            'BLPOP'                     => 'Predis\Command\ListPopFirstBlocking',
            'BRPOP'                     => 'Predis\Command\ListPopLastBlocking',

            /* commands operating on sorted sets */
            'ZUNIONSTORE'               => 'Predis\Command\ZSetUnionStore',
            'ZINTERSTORE'               => 'Predis\Command\ZSetIntersectionStore',
            'ZCOUNT'                    => 'Predis\Command\ZSetCount',
            'ZRANK'                     => 'Predis\Command\ZSetRank',
            'ZREVRANK'                  => 'Predis\Command\ZSetReverseRank',
            'ZREMRANGEBYRANK'           => 'Predis\Command\ZSetRemoveRangeByRank',

            /* commands operating on hashes */
            'HSET'                      => 'Predis\Command\HashSet',
            'HSETNX'                    => 'Predis\Command\HashSetPreserve',
            'HMSET'                     => 'Predis\Command\HashSetMultiple',
            'HINCRBY'                   => 'Predis\Command\HashIncrementBy',
            'HGET'                      => 'Predis\Command\HashGet',
            'HMGET'                     => 'Predis\Command\HashGetMultiple',
            'HDEL'                      => 'Predis\Command\HashDelete',
            'HEXISTS'                   => 'Predis\Command\HashExists',
            'HLEN'                      => 'Predis\Command\HashLength',
            'HKEYS'                     => 'Predis\Command\HashKeys',
            'HVALS'                     => 'Predis\Command\HashValues',
            'HGETALL'                   => 'Predis\Command\HashGetAll',

            /* transactions */
            'MULTI'                     => 'Predis\Command\TransactionMulti',
            'EXEC'                      => 'Predis\Command\TransactionExec',
            'DISCARD'                   => 'Predis\Command\TransactionDiscard',

            /* publish - subscribe */
            'SUBSCRIBE'                 => 'Predis\Command\PubSubSubscribe',
            'UNSUBSCRIBE'               => 'Predis\Command\PubSubUnsubscribe',
            'PSUBSCRIBE'                => 'Predis\Command\PubSubSubscribeByPattern',
            'PUNSUBSCRIBE'              => 'Predis\Command\PubSubUnsubscribeByPattern',
            'PUBLISH'                   => 'Predis\Command\PubSubPublish',

            /* remote server control commands */
            'CONFIG'                    => 'Predis\Command\ServerConfig',

            /* ---------------- Redis 2.2 ---------------- */

            /* commands operating on the key space */
            'PERSIST'                   => 'Predis\Command\KeyPersist',

            /* commands operating on string values */
            'STRLEN'                    => 'Predis\Command\StringStrlen',
            'SETRANGE'                  => 'Predis\Command\StringSetRange',
            'GETRANGE'                  => 'Predis\Command\StringGetRange',
            'SETBIT'                    => 'Predis\Command\StringSetBit',
            'GETBIT'                    => 'Predis\Command\StringGetBit',

            /* commands operating on lists */
            'RPUSHX'                    => 'Predis\Command\ListPushTailX',
            'LPUSHX'                    => 'Predis\Command\ListPushHeadX',
            'LINSERT'                   => 'Predis\Command\ListInsert',
            'BRPOPLPUSH'                => 'Predis\Command\ListPopLastPushHeadBlocking',

            /* commands operating on sorted sets */
            'ZREVRANGEBYSCORE'          => 'Predis\Command\ZSetReverseRangeByScore',

            /* transactions */
            'WATCH'                     => 'Predis\Command\TransactionWatch',
            'UNWATCH'                   => 'Predis\Command\TransactionUnwatch',

            /* remote server control commands */
            'OBJECT'                    => 'Predis\Command\ServerObject',
            'SLOWLOG'                   => 'Predis\Command\ServerSlowlog',

            /* ---------------- Redis 2.4 ---------------- */

            /* remote server control commands */
            'CLIENT'                    => 'Predis\Command\ServerClient',
        );
    }
}

/**
 * Server profile for Redis 2.0.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class RedisVersion200 extends RedisProfile
{
    /**
     * {@inheritdoc}
     */
    public function getVersion()
    {
        return '2.0';
    }

    /**
     * {@inheritdoc}
     */
    public function getSupportedCommands()
    {
        return array(
            /* ---------------- Redis 1.2 ---------------- */

            /* commands operating on the key space */
            'EXISTS'                    => 'Predis\Command\KeyExists',
            'DEL'                       => 'Predis\Command\KeyDelete',
            'TYPE'                      => 'Predis\Command\KeyType',
            'KEYS'                      => 'Predis\Command\KeyKeys',
            'RANDOMKEY'                 => 'Predis\Command\KeyRandom',
            'RENAME'                    => 'Predis\Command\KeyRename',
            'RENAMENX'                  => 'Predis\Command\KeyRenamePreserve',
            'EXPIRE'                    => 'Predis\Command\KeyExpire',
            'EXPIREAT'                  => 'Predis\Command\KeyExpireAt',
            'TTL'                       => 'Predis\Command\KeyTimeToLive',
            'MOVE'                      => 'Predis\Command\KeyMove',
            'SORT'                      => 'Predis\Command\KeySort',

            /* commands operating on string values */
            'SET'                       => 'Predis\Command\StringSet',
            'SETNX'                     => 'Predis\Command\StringSetPreserve',
            'MSET'                      => 'Predis\Command\StringSetMultiple',
            'MSETNX'                    => 'Predis\Command\StringSetMultiplePreserve',
            'GET'                       => 'Predis\Command\StringGet',
            'MGET'                      => 'Predis\Command\StringGetMultiple',
            'GETSET'                    => 'Predis\Command\StringGetSet',
            'INCR'                      => 'Predis\Command\StringIncrement',
            'INCRBY'                    => 'Predis\Command\StringIncrementBy',
            'DECR'                      => 'Predis\Command\StringDecrement',
            'DECRBY'                    => 'Predis\Command\StringDecrementBy',

            /* commands operating on lists */
            'RPUSH'                     => 'Predis\Command\ListPushTail',
            'LPUSH'                     => 'Predis\Command\ListPushHead',
            'LLEN'                      => 'Predis\Command\ListLength',
            'LRANGE'                    => 'Predis\Command\ListRange',
            'LTRIM'                     => 'Predis\Command\ListTrim',
            'LINDEX'                    => 'Predis\Command\ListIndex',
            'LSET'                      => 'Predis\Command\ListSet',
            'LREM'                      => 'Predis\Command\ListRemove',
            'LPOP'                      => 'Predis\Command\ListPopFirst',
            'RPOP'                      => 'Predis\Command\ListPopLast',
            'RPOPLPUSH'                 => 'Predis\Command\ListPopLastPushHead',

            /* commands operating on sets */
            'SADD'                      => 'Predis\Command\SetAdd',
            'SREM'                      => 'Predis\Command\SetRemove',
            'SPOP'                      => 'Predis\Command\SetPop',
            'SMOVE'                     => 'Predis\Command\SetMove',
            'SCARD'                     => 'Predis\Command\SetCardinality',
            'SISMEMBER'                 => 'Predis\Command\SetIsMember',
            'SINTER'                    => 'Predis\Command\SetIntersection',
            'SINTERSTORE'               => 'Predis\Command\SetIntersectionStore',
            'SUNION'                    => 'Predis\Command\SetUnion',
            'SUNIONSTORE'               => 'Predis\Command\SetUnionStore',
            'SDIFF'                     => 'Predis\Command\SetDifference',
            'SDIFFSTORE'                => 'Predis\Command\SetDifferenceStore',
            'SMEMBERS'                  => 'Predis\Command\SetMembers',
            'SRANDMEMBER'               => 'Predis\Command\SetRandomMember',

            /* commands operating on sorted sets */
            'ZADD'                      => 'Predis\Command\ZSetAdd',
            'ZINCRBY'                   => 'Predis\Command\ZSetIncrementBy',
            'ZREM'                      => 'Predis\Command\ZSetRemove',
            'ZRANGE'                    => 'Predis\Command\ZSetRange',
            'ZREVRANGE'                 => 'Predis\Command\ZSetReverseRange',
            'ZRANGEBYSCORE'             => 'Predis\Command\ZSetRangeByScore',
            'ZCARD'                     => 'Predis\Command\ZSetCardinality',
            'ZSCORE'                    => 'Predis\Command\ZSetScore',
            'ZREMRANGEBYSCORE'          => 'Predis\Command\ZSetRemoveRangeByScore',

            /* connection related commands */
            'PING'                      => 'Predis\Command\ConnectionPing',
            'AUTH'                      => 'Predis\Command\ConnectionAuth',
            'SELECT'                    => 'Predis\Command\ConnectionSelect',
            'ECHO'                      => 'Predis\Command\ConnectionEcho',
            'QUIT'                      => 'Predis\Command\ConnectionQuit',

            /* remote server control commands */
            'INFO'                      => 'Predis\Command\ServerInfo',
            'SLAVEOF'                   => 'Predis\Command\ServerSlaveOf',
            'MONITOR'                   => 'Predis\Command\ServerMonitor',
            'DBSIZE'                    => 'Predis\Command\ServerDatabaseSize',
            'FLUSHDB'                   => 'Predis\Command\ServerFlushDatabase',
            'FLUSHALL'                  => 'Predis\Command\ServerFlushAll',
            'SAVE'                      => 'Predis\Command\ServerSave',
            'BGSAVE'                    => 'Predis\Command\ServerBackgroundSave',
            'LASTSAVE'                  => 'Predis\Command\ServerLastSave',
            'SHUTDOWN'                  => 'Predis\Command\ServerShutdown',
            'BGREWRITEAOF'              => 'Predis\Command\ServerBackgroundRewriteAOF',

            /* ---------------- Redis 2.0 ---------------- */

            /* commands operating on string values */
            'SETEX'                     => 'Predis\Command\StringSetExpire',
            'APPEND'                    => 'Predis\Command\StringAppend',
            'SUBSTR'                    => 'Predis\Command\StringSubstr',

            /* commands operating on lists */
            'BLPOP'                     => 'Predis\Command\ListPopFirstBlocking',
            'BRPOP'                     => 'Predis\Command\ListPopLastBlocking',

            /* commands operating on sorted sets */
            'ZUNIONSTORE'               => 'Predis\Command\ZSetUnionStore',
            'ZINTERSTORE'               => 'Predis\Command\ZSetIntersectionStore',
            'ZCOUNT'                    => 'Predis\Command\ZSetCount',
            'ZRANK'                     => 'Predis\Command\ZSetRank',
            'ZREVRANK'                  => 'Predis\Command\ZSetReverseRank',
            'ZREMRANGEBYRANK'           => 'Predis\Command\ZSetRemoveRangeByRank',

            /* commands operating on hashes */
            'HSET'                      => 'Predis\Command\HashSet',
            'HSETNX'                    => 'Predis\Command\HashSetPreserve',
            'HMSET'                     => 'Predis\Command\HashSetMultiple',
            'HINCRBY'                   => 'Predis\Command\HashIncrementBy',
            'HGET'                      => 'Predis\Command\HashGet',
            'HMGET'                     => 'Predis\Command\HashGetMultiple',
            'HDEL'                      => 'Predis\Command\HashDelete',
            'HEXISTS'                   => 'Predis\Command\HashExists',
            'HLEN'                      => 'Predis\Command\HashLength',
            'HKEYS'                     => 'Predis\Command\HashKeys',
            'HVALS'                     => 'Predis\Command\HashValues',
            'HGETALL'                   => 'Predis\Command\HashGetAll',

            /* transactions */
            'MULTI'                     => 'Predis\Command\TransactionMulti',
            'EXEC'                      => 'Predis\Command\TransactionExec',
            'DISCARD'                   => 'Predis\Command\TransactionDiscard',

            /* publish - subscribe */
            'SUBSCRIBE'                 => 'Predis\Command\PubSubSubscribe',
            'UNSUBSCRIBE'               => 'Predis\Command\PubSubUnsubscribe',
            'PSUBSCRIBE'                => 'Predis\Command\PubSubSubscribeByPattern',
            'PUNSUBSCRIBE'              => 'Predis\Command\PubSubUnsubscribeByPattern',
            'PUBLISH'                   => 'Predis\Command\PubSubPublish',

            /* remote server control commands */
            'CONFIG'                    => 'Predis\Command\ServerConfig',
        );
    }
}

/**
 * Server profile for the current unstable version of Redis.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class RedisUnstable extends RedisVersion300
{
    /**
     * {@inheritdoc}
     */
    public function getVersion()
    {
        return '3.0';
    }

    /**
     * {@inheritdoc}
     */
    public function getSupportedCommands()
    {
        return array_merge(parent::getSupportedCommands(), array());
    }
}

/**
 * Factory class for creating profile instances from strings.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
final class Factory
{
    private static $profiles = array(
        '2.0'     => 'Predis\Profile\RedisVersion200',
        '2.2'     => 'Predis\Profile\RedisVersion220',
        '2.4'     => 'Predis\Profile\RedisVersion240',
        '2.6'     => 'Predis\Profile\RedisVersion260',
        '2.8'     => 'Predis\Profile\RedisVersion280',
        '3.0'     => 'Predis\Profile\RedisVersion300',
        'default' => 'Predis\Profile\RedisVersion300',
        'dev'     => 'Predis\Profile\RedisUnstable',
    );

    /**
     *
     */
    private function __construct()
    {
        // NOOP
    }

    /**
     * Returns the default server profile.
     *
     * @return ProfileInterface
     */
    public static function getDefault()
    {
        return self::get('default');
    }

    /**
     * Returns the development server profile.
     *
     * @return ProfileInterface
     */
    public static function getDevelopment()
    {
        return self::get('dev');
    }

    /**
     * Registers a new server profile.
     *
     * @param string $alias Profile version or alias.
     * @param string $class FQN of a class implementing Predis\Profile\ProfileInterface.
     *
     * @throws \InvalidArgumentException
     */
    public static function define($alias, $class)
    {
        $reflection = new ReflectionClass($class);

        if (!$reflection->isSubclassOf('Predis\Profile\ProfileInterface')) {
            throw new InvalidArgumentException("The class '$class' is not a valid profile class.");
        }

        self::$profiles[$alias] = $class;
    }

    /**
     * Returns the specified server profile.
     *
     * @param string $version Profile version or alias.
     *
     * @return ProfileInterface
     *
     * @throws ClientException
     */
    public static function get($version)
    {
        if (!isset(self::$profiles[$version])) {
            throw new ClientException("Unknown server profile: '$version'.");
        }

        $profile = self::$profiles[$version];

        return new $profile();
    }
}

/**
 * Server profile for Redis 2.2.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class RedisVersion220 extends RedisProfile
{
    /**
     * {@inheritdoc}
     */
    public function getVersion()
    {
        return '2.2';
    }

    /**
     * {@inheritdoc}
     */
    public function getSupportedCommands()
    {
        return array(
            /* ---------------- Redis 1.2 ---------------- */

            /* commands operating on the key space */
            'EXISTS'                    => 'Predis\Command\KeyExists',
            'DEL'                       => 'Predis\Command\KeyDelete',
            'TYPE'                      => 'Predis\Command\KeyType',
            'KEYS'                      => 'Predis\Command\KeyKeys',
            'RANDOMKEY'                 => 'Predis\Command\KeyRandom',
            'RENAME'                    => 'Predis\Command\KeyRename',
            'RENAMENX'                  => 'Predis\Command\KeyRenamePreserve',
            'EXPIRE'                    => 'Predis\Command\KeyExpire',
            'EXPIREAT'                  => 'Predis\Command\KeyExpireAt',
            'TTL'                       => 'Predis\Command\KeyTimeToLive',
            'MOVE'                      => 'Predis\Command\KeyMove',
            'SORT'                      => 'Predis\Command\KeySort',

            /* commands operating on string values */
            'SET'                       => 'Predis\Command\StringSet',
            'SETNX'                     => 'Predis\Command\StringSetPreserve',
            'MSET'                      => 'Predis\Command\StringSetMultiple',
            'MSETNX'                    => 'Predis\Command\StringSetMultiplePreserve',
            'GET'                       => 'Predis\Command\StringGet',
            'MGET'                      => 'Predis\Command\StringGetMultiple',
            'GETSET'                    => 'Predis\Command\StringGetSet',
            'INCR'                      => 'Predis\Command\StringIncrement',
            'INCRBY'                    => 'Predis\Command\StringIncrementBy',
            'DECR'                      => 'Predis\Command\StringDecrement',
            'DECRBY'                    => 'Predis\Command\StringDecrementBy',

            /* commands operating on lists */
            'RPUSH'                     => 'Predis\Command\ListPushTail',
            'LPUSH'                     => 'Predis\Command\ListPushHead',
            'LLEN'                      => 'Predis\Command\ListLength',
            'LRANGE'                    => 'Predis\Command\ListRange',
            'LTRIM'                     => 'Predis\Command\ListTrim',
            'LINDEX'                    => 'Predis\Command\ListIndex',
            'LSET'                      => 'Predis\Command\ListSet',
            'LREM'                      => 'Predis\Command\ListRemove',
            'LPOP'                      => 'Predis\Command\ListPopFirst',
            'RPOP'                      => 'Predis\Command\ListPopLast',
            'RPOPLPUSH'                 => 'Predis\Command\ListPopLastPushHead',

            /* commands operating on sets */
            'SADD'                      => 'Predis\Command\SetAdd',
            'SREM'                      => 'Predis\Command\SetRemove',
            'SPOP'                      => 'Predis\Command\SetPop',
            'SMOVE'                     => 'Predis\Command\SetMove',
            'SCARD'                     => 'Predis\Command\SetCardinality',
            'SISMEMBER'                 => 'Predis\Command\SetIsMember',
            'SINTER'                    => 'Predis\Command\SetIntersection',
            'SINTERSTORE'               => 'Predis\Command\SetIntersectionStore',
            'SUNION'                    => 'Predis\Command\SetUnion',
            'SUNIONSTORE'               => 'Predis\Command\SetUnionStore',
            'SDIFF'                     => 'Predis\Command\SetDifference',
            'SDIFFSTORE'                => 'Predis\Command\SetDifferenceStore',
            'SMEMBERS'                  => 'Predis\Command\SetMembers',
            'SRANDMEMBER'               => 'Predis\Command\SetRandomMember',

            /* commands operating on sorted sets */
            'ZADD'                      => 'Predis\Command\ZSetAdd',
            'ZINCRBY'                   => 'Predis\Command\ZSetIncrementBy',
            'ZREM'                      => 'Predis\Command\ZSetRemove',
            'ZRANGE'                    => 'Predis\Command\ZSetRange',
            'ZREVRANGE'                 => 'Predis\Command\ZSetReverseRange',
            'ZRANGEBYSCORE'             => 'Predis\Command\ZSetRangeByScore',
            'ZCARD'                     => 'Predis\Command\ZSetCardinality',
            'ZSCORE'                    => 'Predis\Command\ZSetScore',
            'ZREMRANGEBYSCORE'          => 'Predis\Command\ZSetRemoveRangeByScore',

            /* connection related commands */
            'PING'                      => 'Predis\Command\ConnectionPing',
            'AUTH'                      => 'Predis\Command\ConnectionAuth',
            'SELECT'                    => 'Predis\Command\ConnectionSelect',
            'ECHO'                      => 'Predis\Command\ConnectionEcho',
            'QUIT'                      => 'Predis\Command\ConnectionQuit',

            /* remote server control commands */
            'INFO'                      => 'Predis\Command\ServerInfo',
            'SLAVEOF'                   => 'Predis\Command\ServerSlaveOf',
            'MONITOR'                   => 'Predis\Command\ServerMonitor',
            'DBSIZE'                    => 'Predis\Command\ServerDatabaseSize',
            'FLUSHDB'                   => 'Predis\Command\ServerFlushDatabase',
            'FLUSHALL'                  => 'Predis\Command\ServerFlushAll',
            'SAVE'                      => 'Predis\Command\ServerSave',
            'BGSAVE'                    => 'Predis\Command\ServerBackgroundSave',
            'LASTSAVE'                  => 'Predis\Command\ServerLastSave',
            'SHUTDOWN'                  => 'Predis\Command\ServerShutdown',
            'BGREWRITEAOF'              => 'Predis\Command\ServerBackgroundRewriteAOF',

            /* ---------------- Redis 2.0 ---------------- */

            /* commands operating on string values */
            'SETEX'                     => 'Predis\Command\StringSetExpire',
            'APPEND'                    => 'Predis\Command\StringAppend',
            'SUBSTR'                    => 'Predis\Command\StringSubstr',

            /* commands operating on lists */
            'BLPOP'                     => 'Predis\Command\ListPopFirstBlocking',
            'BRPOP'                     => 'Predis\Command\ListPopLastBlocking',

            /* commands operating on sorted sets */
            'ZUNIONSTORE'               => 'Predis\Command\ZSetUnionStore',
            'ZINTERSTORE'               => 'Predis\Command\ZSetIntersectionStore',
            'ZCOUNT'                    => 'Predis\Command\ZSetCount',
            'ZRANK'                     => 'Predis\Command\ZSetRank',
            'ZREVRANK'                  => 'Predis\Command\ZSetReverseRank',
            'ZREMRANGEBYRANK'           => 'Predis\Command\ZSetRemoveRangeByRank',

            /* commands operating on hashes */
            'HSET'                      => 'Predis\Command\HashSet',
            'HSETNX'                    => 'Predis\Command\HashSetPreserve',
            'HMSET'                     => 'Predis\Command\HashSetMultiple',
            'HINCRBY'                   => 'Predis\Command\HashIncrementBy',
            'HGET'                      => 'Predis\Command\HashGet',
            'HMGET'                     => 'Predis\Command\HashGetMultiple',
            'HDEL'                      => 'Predis\Command\HashDelete',
            'HEXISTS'                   => 'Predis\Command\HashExists',
            'HLEN'                      => 'Predis\Command\HashLength',
            'HKEYS'                     => 'Predis\Command\HashKeys',
            'HVALS'                     => 'Predis\Command\HashValues',
            'HGETALL'                   => 'Predis\Command\HashGetAll',

            /* transactions */
            'MULTI'                     => 'Predis\Command\TransactionMulti',
            'EXEC'                      => 'Predis\Command\TransactionExec',
            'DISCARD'                   => 'Predis\Command\TransactionDiscard',

            /* publish - subscribe */
            'SUBSCRIBE'                 => 'Predis\Command\PubSubSubscribe',
            'UNSUBSCRIBE'               => 'Predis\Command\PubSubUnsubscribe',
            'PSUBSCRIBE'                => 'Predis\Command\PubSubSubscribeByPattern',
            'PUNSUBSCRIBE'              => 'Predis\Command\PubSubUnsubscribeByPattern',
            'PUBLISH'                   => 'Predis\Command\PubSubPublish',

            /* remote server control commands */
            'CONFIG'                    => 'Predis\Command\ServerConfig',

            /* ---------------- Redis 2.2 ---------------- */

            /* commands operating on the key space */
            'PERSIST'                   => 'Predis\Command\KeyPersist',

            /* commands operating on string values */
            'STRLEN'                    => 'Predis\Command\StringStrlen',
            'SETRANGE'                  => 'Predis\Command\StringSetRange',
            'GETRANGE'                  => 'Predis\Command\StringGetRange',
            'SETBIT'                    => 'Predis\Command\StringSetBit',
            'GETBIT'                    => 'Predis\Command\StringGetBit',

            /* commands operating on lists */
            'RPUSHX'                    => 'Predis\Command\ListPushTailX',
            'LPUSHX'                    => 'Predis\Command\ListPushHeadX',
            'LINSERT'                   => 'Predis\Command\ListInsert',
            'BRPOPLPUSH'                => 'Predis\Command\ListPopLastPushHeadBlocking',

            /* commands operating on sorted sets */
            'ZREVRANGEBYSCORE'          => 'Predis\Command\ZSetReverseRangeByScore',

            /* transactions */
            'WATCH'                     => 'Predis\Command\TransactionWatch',
            'UNWATCH'                   => 'Predis\Command\TransactionUnwatch',

            /* remote server control commands */
            'OBJECT'                    => 'Predis\Command\ServerObject',
            'SLOWLOG'                   => 'Predis\Command\ServerSlowlog',
        );
    }
}

/* --------------------------------------------------------------------------- */

namespace Predis;

use InvalidArgumentException;
use UnexpectedValueException;
use Predis\Command\CommandInterface;
use Predis\Command\RawCommand;
use Predis\Command\ScriptCommand;
use Predis\Configuration\Options;
use Predis\Configuration\OptionsInterface;
use Predis\Connection\ConnectionInterface;
use Predis\Connection\AggregateConnectionInterface;
use Predis\Connection\ParametersInterface;
use Predis\Monitor\Consumer as MonitorConsumer;
use Predis\Pipeline\Pipeline;
use Predis\PubSub\Consumer as PubSubConsumer;
use Predis\Response\ErrorInterface as ErrorResponseInterface;
use Predis\Response\ResponseInterface;
use Predis\Response\ServerException;
use Predis\Transaction\MultiExec as MultiExecTransaction;
use Predis\Profile\ProfileInterface;
use Exception;
use Predis\Connection\NodeConnectionInterface;

/**
 * Base exception class for Predis-related errors.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
abstract class PredisException extends Exception
{
}

/**
 * Interface defining a client-side context such as a pipeline or transaction.
 *
 * @method $this del(array $keys)
 * @method $this dump($key)
 * @method $this exists($key)
 * @method $this expire($key, $seconds)
 * @method $this expireat($key, $timestamp)
 * @method $this keys($pattern)
 * @method $this move($key, $db)
 * @method $this object($subcommand, $key)
 * @method $this persist($key)
 * @method $this pexpire($key, $milliseconds)
 * @method $this pexpireat($key, $timestamp)
 * @method $this pttl($key)
 * @method $this randomkey()
 * @method $this rename($key, $target)
 * @method $this renamenx($key, $target)
 * @method $this scan($cursor, array $options = null)
 * @method $this sort($key, array $options = null)
 * @method $this ttl($key)
 * @method $this type($key)
 * @method $this append($key, $value)
 * @method $this bitcount($key, $start = null, $end = null)
 * @method $this bitop($operation, $destkey, $key)
 * @method $this decr($key)
 * @method $this decrby($key, $decrement)
 * @method $this get($key)
 * @method $this getbit($key, $offset)
 * @method $this getrange($key, $start, $end)
 * @method $this getset($key, $value)
 * @method $this incr($key)
 * @method $this incrby($key, $increment)
 * @method $this incrbyfloat($key, $increment)
 * @method $this mget(array $keys)
 * @method $this mset(array $dictionary)
 * @method $this msetnx(array $dictionary)
 * @method $this psetex($key, $milliseconds, $value)
 * @method $this set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null)
 * @method $this setbit($key, $offset, $value)
 * @method $this setex($key, $seconds, $value)
 * @method $this setnx($key, $value)
 * @method $this setrange($key, $offset, $value)
 * @method $this strlen($key)
 * @method $this hdel($key, array $fields)
 * @method $this hexists($key, $field)
 * @method $this hget($key, $field)
 * @method $this hgetall($key)
 * @method $this hincrby($key, $field, $increment)
 * @method $this hincrbyfloat($key, $field, $increment)
 * @method $this hkeys($key)
 * @method $this hlen($key)
 * @method $this hmget($key, array $fields)
 * @method $this hmset($key, array $dictionary)
 * @method $this hscan($key, $cursor, array $options = null)
 * @method $this hset($key, $field, $value)
 * @method $this hsetnx($key, $field, $value)
 * @method $this hvals($key)
 * @method $this blpop(array $keys, $timeout)
 * @method $this brpop(array $keys, $timeout)
 * @method $this brpoplpush($source, $destination, $timeout)
 * @method $this lindex($key, $index)
 * @method $this linsert($key, $whence, $pivot, $value)
 * @method $this llen($key)
 * @method $this lpop($key)
 * @method $this lpush($key, array $values)
 * @method $this lpushx($key, $value)
 * @method $this lrange($key, $start, $stop)
 * @method $this lrem($key, $count, $value)
 * @method $this lset($key, $index, $value)
 * @method $this ltrim($key, $start, $stop)
 * @method $this rpop($key)
 * @method $this rpoplpush($source, $destination)
 * @method $this rpush($key, array $values)
 * @method $this rpushx($key, $value)
 * @method $this sadd($key, array $members)
 * @method $this scard($key)
 * @method $this sdiff(array $keys)
 * @method $this sdiffstore($destination, array $keys)
 * @method $this sinter(array $keys)
 * @method $this sinterstore($destination, array $keys)
 * @method $this sismember($key, $member)
 * @method $this smembers($key)
 * @method $this smove($source, $destination, $member)
 * @method $this spop($key)
 * @method $this srandmember($key, $count = null)
 * @method $this srem($key, $member)
 * @method $this sscan($key, $cursor, array $options = null)
 * @method $this sunion(array $keys)
 * @method $this sunionstore($destination, array $keys)
 * @method $this zadd($key, array $membersAndScoresDictionary)
 * @method $this zcard($key)
 * @method $this zcount($key, $min, $max)
 * @method $this zincrby($key, $increment, $member)
 * @method $this zinterstore($destination, array $keys, array $options = null)
 * @method $this zrange($key, $start, $stop, array $options = null)
 * @method $this zrangebyscore($key, $min, $max, array $options = null)
 * @method $this zrank($key, $member)
 * @method $this zrem($key, $member)
 * @method $this zremrangebyrank($key, $start, $stop)
 * @method $this zremrangebyscore($key, $min, $max)
 * @method $this zrevrange($key, $start, $stop, array $options = null)
 * @method $this zrevrangebyscore($key, $min, $max, array $options = null)
 * @method $this zrevrank($key, $member)
 * @method $this zunionstore($destination, array $keys, array $options = null)
 * @method $this zscore($key, $member)
 * @method $this zscan($key, $cursor, array $options = null)
 * @method $this zrangebylex($key, $start, $stop, array $options = null)
 * @method $this zremrangebylex($key, $min, $max)
 * @method $this zlexcount($key, $min, $max)
 * @method $this pfadd($key, array $elements)
 * @method $this pfmerge($destinationKey, array $sourceKeys)
 * @method $this pfcount(array $keys)
 * @method $this pubsub($subcommand, $argument)
 * @method $this publish($channel, $message)
 * @method $this discard()
 * @method $this exec()
 * @method $this multi()
 * @method $this unwatch()
 * @method $this watch($key)
 * @method $this eval($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
 * @method $this evalsha($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
 * @method $this script($subcommand, $argument = null)
 * @method $this auth($password)
 * @method $this echo($message)
 * @method $this ping($message = null)
 * @method $this select($database)
 * @method $this bgrewriteaof()
 * @method $this bgsave()
 * @method $this client($subcommand, $argument = null)
 * @method $this config($subcommand, $argument = null)
 * @method $this dbsize()
 * @method $this flushall()
 * @method $this flushdb()
 * @method $this info($section = null)
 * @method $this lastsave()
 * @method $this save()
 * @method $this slaveof($host, $port)
 * @method $this slowlog($subcommand, $argument = null)
 * @method $this time()
 * @method $this command()
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface ClientContextInterface
{

    /**
     * Sends the specified command instance to Redis.
     *
     * @param CommandInterface $command Command instance.
     *
     * @return mixed
     */
    public function executeCommand(CommandInterface $command);

    /**
     * Sends the specified command with its arguments to Redis.
     *
     * @param string $method    Command ID.
     * @param array  $arguments Arguments for the command.
     *
     * @return mixed
     */
    public function __call($method, $arguments);

    /**
     * Starts the execution of the context.
     *
     * @param mixed $callable Optional callback for execution.
     *
     * @return array
     */
    public function execute($callable = null);
}

/**
 * Base exception class for network-related errors.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
abstract class CommunicationException extends PredisException
{
    private $connection;

    /**
     * @param NodeConnectionInterface $connection     Connection that generated the exception.
     * @param string                  $message        Error message.
     * @param int                     $code           Error code.
     * @param Exception               $innerException Inner exception for wrapping the original error.
     */
    public function __construct(
        NodeConnectionInterface $connection,
        $message = null,
        $code = null,
        Exception $innerException = null
    ) {
        parent::__construct($message, $code, $innerException);
        $this->connection = $connection;
    }

    /**
     * Gets the connection that generated the exception.
     *
     * @return NodeConnectionInterface
     */
    public function getConnection()
    {
        return $this->connection;
    }

    /**
     * Indicates if the receiver should reset the underlying connection.
     *
     * @return bool
     */
    public function shouldResetConnection()
    {
        return true;
    }

    /**
     * Helper method to handle exceptions generated by a connection object.
     *
     * @param CommunicationException $exception Exception.
     *
     * @throws CommunicationException
     */
    public static function handle(CommunicationException $exception)
    {
        if ($exception->shouldResetConnection()) {
            $connection = $exception->getConnection();

            if ($connection->isConnected()) {
                $connection->disconnect();
            }
        }

        throw $exception;
    }
}

/**
 * Interface defining a client able to execute commands against Redis.
 *
 * All the commands exposed by the client generally have the same signature as
 * described by the Redis documentation, but some of them offer an additional
 * and more friendly interface to ease programming which is described in the
 * following list of methods:
 *
 * @method int    del(array $keys)
 * @method string dump($key)
 * @method int    exists($key)
 * @method int    expire($key, $seconds)
 * @method int    expireat($key, $timestamp)
 * @method array  keys($pattern)
 * @method int    move($key, $db)
 * @method mixed  object($subcommand, $key)
 * @method int    persist($key)
 * @method int    pexpire($key, $milliseconds)
 * @method int    pexpireat($key, $timestamp)
 * @method int    pttl($key)
 * @method string randomkey()
 * @method mixed  rename($key, $target)
 * @method int    renamenx($key, $target)
 * @method array  scan($cursor, array $options = null)
 * @method array  sort($key, array $options = null)
 * @method int    ttl($key)
 * @method mixed  type($key)
 * @method int    append($key, $value)
 * @method int    bitcount($key, $start = null, $end = null)
 * @method int    bitop($operation, $destkey, $key)
 * @method int    decr($key)
 * @method int    decrby($key, $decrement)
 * @method string get($key)
 * @method int    getbit($key, $offset)
 * @method string getrange($key, $start, $end)
 * @method string getset($key, $value)
 * @method int    incr($key)
 * @method int    incrby($key, $increment)
 * @method string incrbyfloat($key, $increment)
 * @method array  mget(array $keys)
 * @method mixed  mset(array $dictionary)
 * @method int    msetnx(array $dictionary)
 * @method mixed  psetex($key, $milliseconds, $value)
 * @method mixed  set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null)
 * @method int    setbit($key, $offset, $value)
 * @method int    setex($key, $seconds, $value)
 * @method int    setnx($key, $value)
 * @method int    setrange($key, $offset, $value)
 * @method int    strlen($key)
 * @method int    hdel($key, array $fields)
 * @method int    hexists($key, $field)
 * @method string hget($key, $field)
 * @method array  hgetall($key)
 * @method int    hincrby($key, $field, $increment)
 * @method string hincrbyfloat($key, $field, $increment)
 * @method array  hkeys($key)
 * @method int    hlen($key)
 * @method array  hmget($key, array $fields)
 * @method mixed  hmset($key, array $dictionary)
 * @method array  hscan($key, $cursor, array $options = null)
 * @method int    hset($key, $field, $value)
 * @method int    hsetnx($key, $field, $value)
 * @method array  hvals($key)
 * @method array  blpop(array $keys, $timeout)
 * @method array  brpop(array $keys, $timeout)
 * @method array  brpoplpush($source, $destination, $timeout)
 * @method string lindex($key, $index)
 * @method int    linsert($key, $whence, $pivot, $value)
 * @method int    llen($key)
 * @method string lpop($key)
 * @method int    lpush($key, array $values)
 * @method int    lpushx($key, $value)
 * @method array  lrange($key, $start, $stop)
 * @method int    lrem($key, $count, $value)
 * @method mixed  lset($key, $index, $value)
 * @method mixed  ltrim($key, $start, $stop)
 * @method string rpop($key)
 * @method string rpoplpush($source, $destination)
 * @method int    rpush($key, array $values)
 * @method int    rpushx($key, $value)
 * @method int    sadd($key, array $members)
 * @method int    scard($key)
 * @method array  sdiff(array $keys)
 * @method int    sdiffstore($destination, array $keys)
 * @method array  sinter(array $keys)
 * @method int    sinterstore($destination, array $keys)
 * @method int    sismember($key, $member)
 * @method array  smembers($key)
 * @method int    smove($source, $destination, $member)
 * @method string spop($key)
 * @method string srandmember($key, $count = null)
 * @method int    srem($key, $member)
 * @method array  sscan($key, $cursor, array $options = null)
 * @method array  sunion(array $keys)
 * @method int    sunionstore($destination, array $keys)
 * @method int    zadd($key, array $membersAndScoresDictionary)
 * @method int    zcard($key)
 * @method string zcount($key, $min, $max)
 * @method string zincrby($key, $increment, $member)
 * @method int    zinterstore($destination, array $keys, array $options = null)
 * @method array  zrange($key, $start, $stop, array $options = null)
 * @method array  zrangebyscore($key, $min, $max, array $options = null)
 * @method int    zrank($key, $member)
 * @method int    zrem($key, $member)
 * @method int    zremrangebyrank($key, $start, $stop)
 * @method int    zremrangebyscore($key, $min, $max)
 * @method array  zrevrange($key, $start, $stop, array $options = null)
 * @method array  zrevrangebyscore($key, $min, $max, array $options = null)
 * @method int    zrevrank($key, $member)
 * @method int    zunionstore($destination, array $keys, array $options = null)
 * @method string zscore($key, $member)
 * @method array  zscan($key, $cursor, array $options = null)
 * @method array  zrangebylex($key, $start, $stop, array $options = null)
 * @method int    zremrangebylex($key, $min, $max)
 * @method int    zlexcount($key, $min, $max)
 * @method int    pfadd($key, array $elements)
 * @method mixed  pfmerge($destinationKey, array $sourceKeys)
 * @method int    pfcount(array $keys)
 * @method mixed  pubsub($subcommand, $argument)
 * @method int    publish($channel, $message)
 * @method mixed  discard()
 * @method array  exec()
 * @method mixed  multi()
 * @method mixed  unwatch()
 * @method mixed  watch($key)
 * @method mixed  eval($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
 * @method mixed  evalsha($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
 * @method mixed  script($subcommand, $argument = null)
 * @method mixed  auth($password)
 * @method string echo($message)
 * @method mixed  ping($message = null)
 * @method mixed  select($database)
 * @method mixed  bgrewriteaof()
 * @method mixed  bgsave()
 * @method mixed  client($subcommand, $argument = null)
 * @method mixed  config($subcommand, $argument = null)
 * @method int    dbsize()
 * @method mixed  flushall()
 * @method mixed  flushdb()
 * @method array  info($section = null)
 * @method int    lastsave()
 * @method mixed  save()
 * @method mixed  slaveof($host, $port)
 * @method mixed  slowlog($subcommand, $argument = null)
 * @method array  time()
 * @method array  command()
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface ClientInterface
{
    /**
     * Returns the server profile used by the client.
     *
     * @return ProfileInterface
     */
    public function getProfile();

    /**
     * Returns the client options specified upon initialization.
     *
     * @return OptionsInterface
     */
    public function getOptions();

    /**
     * Opens the underlying connection to the server.
     */
    public function connect();

    /**
     * Closes the underlying connection from the server.
     */
    public function disconnect();

    /**
     * Returns the underlying connection instance.
     *
     * @return ConnectionInterface
     */
    public function getConnection();

    /**
     * Creates a new instance of the specified Redis command.
     *
     * @param string $method    Command ID.
     * @param array  $arguments Arguments for the command.
     *
     * @return CommandInterface
     */
    public function createCommand($method, $arguments = array());

    /**
     * Executes the specified Redis command.
     *
     * @param CommandInterface $command Command instance.
     *
     * @return mixed
     */
    public function executeCommand(CommandInterface $command);

    /**
     * Creates a Redis command with the specified arguments and sends a request
     * to the server.
     *
     * @param string $method    Command ID.
     * @param array  $arguments Arguments for the command.
     *
     * @return mixed
     */
    public function __call($method, $arguments);
}

/**
 * Exception class thrown when trying to use features not supported by certain
 * classes or abstractions of Predis.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class NotSupportedException extends PredisException
{
}

/**
 * Exception class that identifies client-side errors.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ClientException extends PredisException
{
}

/**
 * Client class used for connecting and executing commands on Redis.
 *
 * This is the main high-level abstraction of Predis upon which various other
 * abstractions are built. Internally it aggregates various other classes each
 * one with its own responsibility and scope.
 *
 * {@inheritdoc}
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class Client implements ClientInterface
{
    const VERSION = '1.0.1';

    protected $connection;
    protected $options;
    private $profile;

    /**
     * @param mixed $parameters Connection parameters for one or more servers.
     * @param mixed $options    Options to configure some behaviours of the client.
     */
    public function __construct($parameters = null, $options = null)
    {
        $this->options = $this->createOptions($options ?: array());
        $this->connection = $this->createConnection($parameters ?: array());
        $this->profile = $this->options->profile;
    }

    /**
     * Creates a new instance of Predis\Configuration\Options from different
     * types of arguments or simply returns the passed argument if it is an
     * instance of Predis\Configuration\OptionsInterface.
     *
     * @param mixed $options Client options.
     *
     * @return OptionsInterface
     *
     * @throws \InvalidArgumentException
     */
    protected function createOptions($options)
    {
        if (is_array($options)) {
            return new Options($options);
        }

        if ($options instanceof OptionsInterface) {
            return $options;
        }

        throw new InvalidArgumentException("Invalid type for client options.");
    }

    /**
     * Creates single or aggregate connections from different types of arguments
     * (string, array) or returns the passed argument if it is an instance of a
     * class implementing Predis\Connection\ConnectionInterface.
     *
     * Accepted types for connection parameters are:
     *
     *  - Instance of Predis\Connection\ConnectionInterface.
     *  - Instance of Predis\Connection\ParametersInterface.
     *  - Array
     *  - String
     *  - Callable
     *
     * @param mixed $parameters Connection parameters or connection instance.
     *
     * @return ConnectionInterface
     *
     * @throws \InvalidArgumentException
     */
    protected function createConnection($parameters)
    {
        if ($parameters instanceof ConnectionInterface) {
            return $parameters;
        }

        if ($parameters instanceof ParametersInterface || is_string($parameters)) {
            return $this->options->connections->create($parameters);
        }

        if (is_array($parameters)) {
            if (!isset($parameters[0])) {
                return $this->options->connections->create($parameters);
            }

            $options = $this->options;

            if ($options->defined('aggregate')) {
                $initializer = $this->getConnectionInitializerWrapper($options->aggregate);
                $connection = $initializer($parameters, $options);
            } else {
                if ($options->defined('replication') && $replication = $options->replication) {
                    $connection = $replication;
                } else {
                    $connection = $options->cluster;
                }

                $options->connections->aggregate($connection, $parameters);
            }

            return $connection;
        }

        if (is_callable($parameters)) {
            $initializer = $this->getConnectionInitializerWrapper($parameters);
            $connection = $initializer($this->options);

            return $connection;
        }

        throw new InvalidArgumentException('Invalid type for connection parameters.');
    }

    /**
     * Wraps a callable to make sure that its returned value represents a valid
     * connection type.
     *
     * @param mixed $callable
     *
     * @return \Closure
     */
    protected function getConnectionInitializerWrapper($callable)
    {
        return function () use ($callable) {
            $connection = call_user_func_array($callable, func_get_args());

            if (!$connection instanceof ConnectionInterface) {
                throw new UnexpectedValueException(
                    'The callable connection initializer returned an invalid type.'
                );
            }

            return $connection;
        };
    }

    /**
     * {@inheritdoc}
     */
    public function getProfile()
    {
        return $this->profile;
    }

    /**
     * {@inheritdoc}
     */
    public function getOptions()
    {
        return $this->options;
    }

    /**
     * Creates a new client instance for the specified connection ID or alias,
     * only when working with an aggregate connection (cluster, replication).
     * The new client instances uses the same options of the original one.
     *
     * @param string $connectionID Identifier of a connection.
     *
     * @return Client
     *
     * @throws \InvalidArgumentException
     */
    public function getClientFor($connectionID)
    {
        if (!$connection = $this->getConnectionById($connectionID)) {
            throw new InvalidArgumentException("Invalid connection ID: $connectionID.");
        }

        return new static($connection, $this->options);
    }

    /**
     * Opens the underlying connection and connects to the server.
     */
    public function connect()
    {
        $this->connection->connect();
    }

    /**
     * Closes the underlying connection and disconnects from the server.
     */
    public function disconnect()
    {
        $this->connection->disconnect();
    }

    /**
     * Closes the underlying connection and disconnects from the server.
     *
     * This is the same as `Client::disconnect()` as it does not actually send
     * the `QUIT` command to Redis, but simply closes the connection.
     */
    public function quit()
    {
        $this->disconnect();
    }

    /**
     * Returns the current state of the underlying connection.
     *
     * @return bool
     */
    public function isConnected()
    {
        return $this->connection->isConnected();
    }

    /**
     * {@inheritdoc}
     */
    public function getConnection()
    {
        return $this->connection;
    }

    /**
     * Retrieves the specified connection from the aggregate connection when the
     * client is in cluster or replication mode.
     *
     * @param string $connectionID Index or alias of the single connection.
     *
     * @return Connection\NodeConnectionInterface
     *
     * @throws NotSupportedException
     */
    public function getConnectionById($connectionID)
    {
        if (!$this->connection instanceof AggregateConnectionInterface) {
            throw new NotSupportedException(
                'Retrieving connections by ID is supported only by aggregate connections.'
            );
        }

        return $this->connection->getConnectionById($connectionID);
    }

    /**
     * Executes a command without filtering its arguments, parsing the response,
     * applying any prefix to keys or throwing exceptions on Redis errors even
     * regardless of client options.
     *
     * It is possibile to indentify Redis error responses from normal responses
     * using the second optional argument which is populated by reference.
     *
     * @param array $arguments Command arguments as defined by the command signature.
     * @param bool  $error     Set to TRUE when Redis returned an error response.
     *
     * @return mixed
     */
    public function executeRaw(array $arguments, &$error = null)
    {
        $error = false;
        $response = $this->connection->executeCommand(
            new RawCommand($arguments)
        );

        if ($response instanceof ResponseInterface) {
            if ($response instanceof ErrorResponseInterface) {
                $error = true;
            }

            return (string) $response;
        }

        return $response;
    }

    /**
     * {@inheritdoc}
     */
    public function __call($commandID, $arguments)
    {
        return $this->executeCommand(
            $this->createCommand($commandID, $arguments)
        );
    }

    /**
     * {@inheritdoc}
     */
    public function createCommand($commandID, $arguments = array())
    {
        return $this->profile->createCommand($commandID, $arguments);
    }

    /**
     * {@inheritdoc}
     */
    public function executeCommand(CommandInterface $command)
    {
        $response = $this->connection->executeCommand($command);

        if ($response instanceof ResponseInterface) {
            if ($response instanceof ErrorResponseInterface) {
                $response = $this->onErrorResponse($command, $response);
            }

            return $response;
        }

        return $command->parseResponse($response);
    }

    /**
     * Handles -ERR responses returned by Redis.
     *
     * @param CommandInterface       $command  Redis command that generated the error.
     * @param ErrorResponseInterface $response Instance of the error response.
     *
     * @return mixed
     *
     * @throws ServerException
     */
    protected function onErrorResponse(CommandInterface $command, ErrorResponseInterface $response)
    {
        if ($command instanceof ScriptCommand && $response->getErrorType() === 'NOSCRIPT') {
            $eval = $this->createCommand('EVAL');
            $eval->setRawArguments($command->getEvalArguments());

            $response = $this->executeCommand($eval);

            if (!$response instanceof ResponseInterface) {
                $response = $command->parseResponse($response);
            }

            return $response;
        }

        if ($this->options->exceptions) {
            throw new ServerException($response->getMessage());
        }

        return $response;
    }

    /**
     * Executes the specified initializer method on `$this` by adjusting the
     * actual invokation depending on the arity (0, 1 or 2 arguments). This is
     * simply an utility method to create Redis contexts instances since they
     * follow a common initialization path.
     *
     * @param string $initializer Method name.
     * @param array  $argv        Arguments for the method.
     *
     * @return mixed
     */
    private function sharedContextFactory($initializer, $argv = null)
    {
        switch (count($argv)) {
            case 0:
                return $this->$initializer();

            case 1:
                return is_array($argv[0])
                    ? $this->$initializer($argv[0])
                    : $this->$initializer(null, $argv[0]);

            case 2:
                list($arg0, $arg1) = $argv;

                return $this->$initializer($arg0, $arg1);

            default:
                return $this->$initializer($this, $argv);
        }
    }

    /**
     * Creates a new pipeline context and returns it, or returns the results of
     * a pipeline executed inside the optionally provided callable object.
     *
     * @param mixed ... Array of options, a callable for execution, or both.
     *
     * @return Pipeline|array
     */
    public function pipeline(/* arguments */)
    {
        return $this->sharedContextFactory('createPipeline', func_get_args());
    }

    /**
     * Actual pipeline context initializer method.
     *
     * @param array $options  Options for the context.
     * @param mixed $callable Optional callable used to execute the context.
     *
     * @return Pipeline|array
     */
    protected function createPipeline(array $options = null, $callable = null)
    {
        if (isset($options['atomic']) && $options['atomic']) {
            $class = 'Predis\Pipeline\Atomic';
        } elseif (isset($options['fire-and-forget']) && $options['fire-and-forget']) {
            $class = 'Predis\Pipeline\FireAndForget';
        } else {
            $class = 'Predis\Pipeline\Pipeline';
        }

        /*
         * @var ClientContextInterface
         */
        $pipeline = new $class($this);

        if (isset($callable)) {
            return $pipeline->execute($callable);
        }

        return $pipeline;
    }

    /**
     * Creates a new transaction context and returns it, or returns the results
     * of a transaction executed inside the optionally provided callable object.
     *
     * @param mixed ... Array of options, a callable for execution, or both.
     *
     * @return MultiExecTransaction|array
     */
    public function transaction(/* arguments */)
    {
        return $this->sharedContextFactory('createTransaction', func_get_args());
    }

    /**
     * Actual transaction context initializer method.
     *
     * @param array $options  Options for the context.
     * @param mixed $callable Optional callable used to execute the context.
     *
     * @return MultiExecTransaction|array
     */
    protected function createTransaction(array $options = null, $callable = null)
    {
        $transaction = new MultiExecTransaction($this, $options);

        if (isset($callable)) {
            return $transaction->execute($callable);
        }

        return $transaction;
    }

    /**
     * Creates a new publis/subscribe context and returns it, or starts its loop
     * inside the optionally provided callable object.
     *
     * @param mixed ... Array of options, a callable for execution, or both.
     *
     * @return PubSubConsumer|null
     */
    public function pubSubLoop(/* arguments */)
    {
        return $this->sharedContextFactory('createPubSub', func_get_args());
    }

    /**
     * Actual publish/subscribe context initializer method.
     *
     * @param array $options  Options for the context.
     * @param mixed $callable Optional callable used to execute the context.
     *
     * @return PubSubConsumer|null
     */
    protected function createPubSub(array $options = null, $callable = null)
    {
        $pubsub = new PubSubConsumer($this, $options);

        if (!isset($callable)) {
            return $pubsub;
        }

        foreach ($pubsub as $message) {
            if (call_user_func($callable, $pubsub, $message) === false) {
                $pubsub->stop();
            }
        }
    }

    /**
     * Creates a new monitor consumer and returns it.
     *
     * @return MonitorConsumer
     */
    public function monitor()
    {
        return new MonitorConsumer($this);
    }
}

/**
 * Implements a lightweight PSR-0 compliant autoloader for Predis.
 *
 * @author Eric Naeseth <eric@thumbtack.com>
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class Autoloader
{
    private $directory;
    private $prefix;
    private $prefixLength;

    /**
     * @param string $baseDirectory Base directory where the source files are located.
     */
    public function __construct($baseDirectory = __DIR__)
    {
        $this->directory = $baseDirectory;
        $this->prefix = __NAMESPACE__ . '\\';
        $this->prefixLength = strlen($this->prefix);
    }

    /**
     * Registers the autoloader class with the PHP SPL autoloader.
     *
     * @param bool $prepend Prepend the autoloader on the stack instead of appending it.
     */
    public static function register($prepend = false)
    {
        spl_autoload_register(array(new self, 'autoload'), true, $prepend);
    }

    /**
     * Loads a class from a file using its fully qualified name.
     *
     * @param string $className Fully qualified name of a class.
     */
    public function autoload($className)
    {
        if (0 === strpos($className, $this->prefix)) {
            $parts = explode('\\', substr($className, $this->prefixLength));
            $filepath = $this->directory.DIRECTORY_SEPARATOR.implode(DIRECTORY_SEPARATOR, $parts).'.php';

            if (is_file($filepath)) {
                require($filepath);
            }
        }
    }
}

/* --------------------------------------------------------------------------- */

namespace Predis\Configuration;

use InvalidArgumentException;
use Predis\Connection\Aggregate\ClusterInterface;
use Predis\Connection\Aggregate\PredisCluster;
use Predis\Connection\Aggregate\RedisCluster;
use Predis\Connection\Factory;
use Predis\Connection\FactoryInterface;
use Predis\Command\Processor\KeyPrefixProcessor;
use Predis\Command\Processor\ProcessorInterface;
use Predis\Profile\Factory as Predis_Factory;
use Predis\Profile\ProfileInterface;
use Predis\Profile\RedisProfile;
use Predis\Connection\Aggregate\MasterSlaveReplication;
use Predis\Connection\Aggregate\ReplicationInterface;

/**
 * Defines an handler used by Predis\Configuration\Options to filter, validate
 * or return default values for a given option.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface OptionInterface
{
    /**
     * Filters and validates the passed value.
     *
     * @param OptionsInterface $options Options container.
     * @param mixed            $value   Input value.
     *
     * @return mixed
     */
    public function filter(OptionsInterface $options, $value);

    /**
     * Returns the default value for the option.
     *
     * @param OptionsInterface $options Options container.
     *
     * @return mixed
     */
    public function getDefault(OptionsInterface $options);
}

/**
 * Interface defining a container for client options.
 *
 * @property-read mixed aggregate   Custom connection aggregator.
 * @property-read mixed cluster     Aggregate connection for clustering.
 * @property-read mixed connections Connection factory.
 * @property-read mixed exceptions  Toggles exceptions in client for -ERR responses.
 * @property-read mixed prefix      Key prefixing strategy using the given prefix.
 * @property-read mixed profile     Server profile.
 * @property-read mixed replication Aggregate connection for replication.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface OptionsInterface
{
    /**
     * Returns the default value for the given option.
     *
     * @param string $option Name of the option.
     *
     * @return mixed|null
     */
    public function getDefault($option);

    /**
     * Checks if the given option has been set by the user upon initialization.
     *
     * @param string $option Name of the option.
     *
     * @return bool
     */
    public function defined($option);

    /**
     * Checks if the given option has been set and does not evaluate to NULL.
     *
     * @param string $option Name of the option.
     *
     * @return bool
     */
    public function __isset($option);

    /**
     * Returns the value of the given option.
     *
     * @param string $option Name of the option.
     *
     * @return mixed|null
     */
    public function __get($option);
}

/**
 * Configures a command processor that apply the specified prefix string to a
 * series of Redis commands considered prefixable.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class PrefixOption implements OptionInterface
{
    /**
     * {@inheritdoc}
     */
    public function filter(OptionsInterface $options, $value)
    {
        if ($value instanceof ProcessorInterface) {
            return $value;
        }

        return new KeyPrefixProcessor($value);
    }

    /**
     * {@inheritdoc}
     */
    public function getDefault(OptionsInterface $options)
    {
        // NOOP
    }
}

/**
 * Configures the server profile to be used by the client to create command
 * instances depending on the specified version of the Redis server.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ProfileOption implements OptionInterface
{
    /**
     * Sets the commands processors that need to be applied to the profile.
     *
     * @param OptionsInterface $options Client options.
     * @param ProfileInterface $profile Server profile.
     */
    protected function setProcessors(OptionsInterface $options, ProfileInterface $profile)
    {
        if (isset($options->prefix) && $profile instanceof RedisProfile) {
            // NOTE: directly using __get('prefix') is actually a workaround for
            // HHVM 2.3.0. It's correct and respects the options interface, it's
            // just ugly. We will remove this hack when HHVM will fix re-entrant
            // calls to __get() once and for all.

            $profile->setProcessor($options->__get('prefix'));
        }
    }

    /**
     * {@inheritdoc}
     */
    public function filter(OptionsInterface $options, $value)
    {
        if (is_string($value)) {
            $value = Predis_Factory::get($value);
            $this->setProcessors($options, $value);
        } elseif (!$value instanceof ProfileInterface) {
            throw new InvalidArgumentException('Invalid value for the profile option.');
        }

        return $value;
    }

    /**
     * {@inheritdoc}
     */
    public function getDefault(OptionsInterface $options)
    {
        $profile = Predis_Factory::getDefault();
        $this->setProcessors($options, $profile);

        return $profile;
    }
}

/**
 * Configures an aggregate connection used for master/slave replication among
 * multiple Redis nodes.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ReplicationOption implements OptionInterface
{
    /**
     * {@inheritdoc}
     *
     * @todo There's more code than needed due to a bug in filter_var() as
     *       discussed here https://bugs.php.net/bug.php?id=49510 and  different
     *       behaviours when encountering NULL values on PHP 5.3.
     */
    public function filter(OptionsInterface $options, $value)
    {
        if ($value instanceof ReplicationInterface) {
            return $value;
        }

        if (is_bool($value) || $value === null) {
            return $value ? $this->getDefault($options) : null;
        }

        if (
            !is_object($value) &&
            null !== $asbool = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)
        ) {
            return $asbool ? $this->getDefault($options) : null;
        }

        throw new InvalidArgumentException(
            "An instance of type 'Predis\Connection\Aggregate\ReplicationInterface' was expected."
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getDefault(OptionsInterface $options)
    {
        return new MasterSlaveReplication();
    }
}

/**
 * Manages Predis options with filtering, conversion and lazy initialization of
 * values using a mini-DI container approach.
 *
 * {@inheritdoc}
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class Options implements OptionsInterface
{
    protected $input;
    protected $options;
    protected $handlers;

    /**
     * @param array $options Array of options with their values
     */
    public function __construct(array $options = array())
    {
        $this->input = $options;
        $this->options = array();
        $this->handlers = $this->getHandlers();
    }

    /**
     * Ensures that the default options are initialized.
     *
     * @return array
     */
    protected function getHandlers()
    {
        return array(
            'cluster'     => 'Predis\Configuration\ClusterOption',
            'connections' => 'Predis\Configuration\ConnectionFactoryOption',
            'exceptions'  => 'Predis\Configuration\ExceptionsOption',
            'prefix'      => 'Predis\Configuration\PrefixOption',
            'profile'     => 'Predis\Configuration\ProfileOption',
            'replication' => 'Predis\Configuration\ReplicationOption',
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getDefault($option)
    {
        if (isset($this->handlers[$option])) {
            $handler = $this->handlers[$option];
            $handler = new $handler();

            return $handler->getDefault($this);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function defined($option)
    {
        return (
            array_key_exists($option, $this->options) ||
            array_key_exists($option, $this->input)
        );
    }

    /**
     * {@inheritdoc}
     */
    public function __isset($option)
    {
        return (
            array_key_exists($option, $this->options) ||
            array_key_exists($option, $this->input)
        ) && $this->__get($option) !== null;
    }

    /**
     * {@inheritdoc}
     */
    public function __get($option)
    {
        if (isset($this->options[$option]) || array_key_exists($option, $this->options)) {
            return $this->options[$option];
        }

        if (isset($this->input[$option]) || array_key_exists($option, $this->input)) {
            $value = $this->input[$option];
            unset($this->input[$option]);

            if (method_exists($value, '__invoke')) {
                $value = $value($this, $option);
            }

            if (isset($this->handlers[$option])) {
                $handler = $this->handlers[$option];
                $handler = new $handler();
                $value = $handler->filter($this, $value);
            }

            return $this->options[$option] = $value;
        }

        if (isset($this->handlers[$option])) {
            return $this->options[$option] = $this->getDefault($option);
        }

        return null;
    }
}

/**
 * Configures a connection factory used by the client to create new connection
 * instances for single Redis nodes.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ConnectionFactoryOption implements OptionInterface
{
    /**
     * {@inheritdoc}
     */
    public function filter(OptionsInterface $options, $value)
    {
        if ($value instanceof FactoryInterface) {
            return $value;
        } elseif (is_array($value)) {
            $factory = $this->getDefault($options);

            foreach ($value as $scheme => $initializer) {
                $factory->define($scheme, $initializer);
            }

            return $factory;
        } else {
            throw new InvalidArgumentException(
                'Invalid value provided for the connections option.'
            );
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getDefault(OptionsInterface $options)
    {
        return new Factory();
    }
}

/**
 * Configures whether consumers (such as the client) should throw exceptions on
 * Redis errors (-ERR responses) or just return instances of error responses.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ExceptionsOption implements OptionInterface
{
    /**
     * {@inheritdoc}
     */
    public function filter(OptionsInterface $options, $value)
    {
        return filter_var($value, FILTER_VALIDATE_BOOLEAN);
    }

    /**
     * {@inheritdoc}
     */
    public function getDefault(OptionsInterface $options)
    {
        return true;
    }
}

/**
 * Configures an aggregate connection used for clustering
 * multiple Redis nodes using various implementations with
 * different algorithms or strategies.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ClusterOption implements OptionInterface
{
    /**
     * Creates a new cluster connection from on a known descriptive name.
     *
     * @param OptionsInterface $options Instance of the client options.
     * @param string           $id      Descriptive identifier of the cluster type (`predis`, `redis-cluster`)
     *
     * @return ClusterInterface|null
     */
    protected function createByDescription(OptionsInterface $options, $id)
    {
        switch ($id) {
            case 'predis':
            case 'predis-cluster':
                return new PredisCluster();

            case 'redis':
            case 'redis-cluster':
                return new RedisCluster($options->connections);

            default:
                return;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function filter(OptionsInterface $options, $value)
    {
        if (is_string($value)) {
            $value = $this->createByDescription($options, $value);
        }

        if (!$value instanceof ClusterInterface) {
            throw new InvalidArgumentException(
                "An instance of type 'Predis\Connection\Aggregate\ClusterInterface' was expected."
            );
        }

        return $value;
    }

    /**
     * {@inheritdoc}
     */
    public function getDefault(OptionsInterface $options)
    {
        return new PredisCluster();
    }
}

/* --------------------------------------------------------------------------- */

namespace Predis\Response;

use Predis\PredisException;

/**
 * Represents a complex response object from Redis.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface ResponseInterface
{
}

/**
 * Represents an error returned by Redis (responses identified by "-" in the
 * Redis protocol) during the execution of an operation on the server.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface ErrorInterface extends ResponseInterface
{
    /**
     * Returns the error message
     *
     * @return string
     */
    public function getMessage();

    /**
     * Returns the error type (e.g. ERR, ASK, MOVED)
     *
     * @return string
     */
    public function getErrorType();
}

/**
 * Represents a status response returned by Redis.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class Status implements ResponseInterface
{
    private static $OK;
    private static $QUEUED;

    private $payload;

    /**
     * @param string $payload Payload of the status response as returned by Redis.
     */
    public function __construct($payload)
    {
        $this->payload = $payload;
    }

    /**
     * Converts the response object to its string representation.
     *
     * @return string
     */
    public function __toString()
    {
        return $this->payload;
    }

    /**
     * Returns the payload of status response.
     *
     * @return string
     */
    public function getPayload()
    {
        return $this->payload;
    }

    /**
     * Returns an instance of a status response object.
     *
     * Common status responses such as OK or QUEUED are cached in order to lower
     * the global memory usage especially when using pipelines.
     *
     * @param string $payload Status response payload.
     *
     * @return string
     */
    public static function get($payload)
    {
        switch ($payload) {
            case 'OK':
            case 'QUEUED':
                if (isset(self::$$payload)) {
                    return self::$$payload;
                }

                return self::$$payload = new self($payload);

            default:
                return new self($payload);
        }
    }
}

/**
 * Represents an error returned by Redis (-ERR responses) during the execution
 * of a command on the server.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class Error implements ErrorInterface
{
    private $message;

    /**
     * @param string $message Error message returned by Redis
     */
    public function __construct($message)
    {
        $this->message = $message;
    }

    /**
     * {@inheritdoc}
     */
    public function getMessage()
    {
        return $this->message;
    }

    /**
     * {@inheritdoc}
     */
    public function getErrorType()
    {
        list($errorType, ) = explode(' ', $this->getMessage(), 2);

        return $errorType;
    }

    /**
     * Converts the object to its string representation.
     *
     * @return string
     */
    public function __toString()
    {
        return $this->getMessage();
    }
}

/**
 * Exception class that identifies server-side Redis errors.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ServerException extends PredisException implements ErrorInterface
{
    /**
     * Gets the type of the error returned by Redis.
     *
     * @return string
     */
    public function getErrorType()
    {
        list($errorType, ) = explode(' ', $this->getMessage(), 2);

        return $errorType;
    }

    /**
     * Converts the exception to an instance of Predis\Response\Error.
     *
     * @return Error
     */
    public function toErrorResponse()
    {
        return new Error($this->getMessage());
    }
}

/* --------------------------------------------------------------------------- */

namespace Predis\Protocol\Text\Handler;

use Predis\CommunicationException;
use Predis\Connection\CompositeConnectionInterface;
use Predis\Protocol\ProtocolException;
use Predis\Response\Error;
use Predis\Response\Status;
use Predis\Response\Iterator\MultiBulk as MultiBulkIterator;

/**
 * Defines a pluggable handler used to parse a particular type of response.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface ResponseHandlerInterface
{
    /**
     * Deserializes a response returned by Redis and reads more data from the
     * connection if needed.
     *
     * @param CompositeConnectionInterface $connection Redis connection.
     * @param string                       $payload    String payload.
     *
     * @return mixed
     */
    public function handle(CompositeConnectionInterface $connection, $payload);
}

/**
 * Handler for the status response type in the standard Redis wire protocol. It
 * translates certain classes of status response to PHP objects or just returns
 * the payload as a string.
 *
 * @link http://redis.io/topics/protocol
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StatusResponse implements ResponseHandlerInterface
{
    /**
     * {@inheritdoc}
     */
    public function handle(CompositeConnectionInterface $connection, $payload)
    {
        return Status::get($payload);
    }
}

/**
 * Handler for the multibulk response type in the standard Redis wire protocol.
 * It returns multibulk responses as iterators that can stream bulk elements.
 *
 * Streamable multibulk responses are not globally supported by the abstractions
 * built-in into Predis, such as transactions or pipelines. Use them with care!
 *
 * @link http://redis.io/topics/protocol
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class StreamableMultiBulkResponse implements ResponseHandlerInterface
{
    /**
     * {@inheritdoc}
     */
    public function handle(CompositeConnectionInterface $connection, $payload)
    {
        $length = (int) $payload;

        if ("$length" != $payload) {
            CommunicationException::handle(new ProtocolException(
                $connection, "Cannot parse '$payload' as a valid length for a multi-bulk response."
            ));
        }

        return new MultiBulkIterator($connection, $length);
    }
}

/**
 * Handler for the multibulk response type in the standard Redis wire protocol.
 * It returns multibulk responses as PHP arrays.
 *
 * @link http://redis.io/topics/protocol
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class MultiBulkResponse implements ResponseHandlerInterface
{
    /**
     * {@inheritdoc}
     */
    public function handle(CompositeConnectionInterface $connection, $payload)
    {
        $length = (int) $payload;

        if ("$length" !== $payload) {
            CommunicationException::handle(new ProtocolException(
                $connection, "Cannot parse '$payload' as a valid length of a multi-bulk response."
            ));
        }

        if ($length === -1) {
            return null;
        }

        $list = array();

        if ($length > 0) {
            $handlersCache = array();
            $reader = $connection->getProtocol()->getResponseReader();

            for ($i = 0; $i < $length; $i++) {
                $header = $connection->readLine();
                $prefix = $header[0];

                if (isset($handlersCache[$prefix])) {
                    $handler = $handlersCache[$prefix];
                } else {
                    $handler = $reader->getHandler($prefix);
                    $handlersCache[$prefix] = $handler;
                }

                $list[$i] = $handler->handle($connection, substr($header, 1));
            }
        }

        return $list;
    }
}

/**
 * Handler for the error response type in the standard Redis wire protocol.
 * It translates the payload to a complex response object for Predis.
 *
 * @link http://redis.io/topics/protocol
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ErrorResponse implements ResponseHandlerInterface
{
    /**
     * {@inheritdoc}
     */
    public function handle(CompositeConnectionInterface $connection, $payload)
    {
        return new Error($payload);
    }
}

/**
 * Handler for the integer response type in the standard Redis wire protocol.
 * It translates the payload an integer or NULL.
 *
 * @link http://redis.io/topics/protocol
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class IntegerResponse implements ResponseHandlerInterface
{
    /**
     * {@inheritdoc}
     */
    public function handle(CompositeConnectionInterface $connection, $payload)
    {
        if (is_numeric($payload)) {
            return (int) $payload;
        }

        if ($payload !== 'nil') {
            CommunicationException::handle(new ProtocolException(
                $connection, "Cannot parse '$payload' as a valid numeric response."
            ));
        }

        return null;
    }
}

/**
 * Handler for the bulk response type in the standard Redis wire protocol.
 * It translates the payload to a string or a NULL.
 *
 * @link http://redis.io/topics/protocol
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class BulkResponse implements ResponseHandlerInterface
{
    /**
     * {@inheritdoc}
     */
    public function handle(CompositeConnectionInterface $connection, $payload)
    {
        $length = (int) $payload;

        if ("$length" !== $payload) {
            CommunicationException::handle(new ProtocolException(
                $connection, "Cannot parse '$payload' as a valid length for a bulk response."
            ));
        }

        if ($length >= 0) {
            return substr($connection->readBuffer($length + 2), 0, -2);
        }

        if ($length == -1) {
            return null;
        }

        CommunicationException::handle(new ProtocolException(
            $connection, "Value '$payload' is not a valid length for a bulk response."
        ));

        return;
    }
}

/* --------------------------------------------------------------------------- */

namespace Predis\Collection\Iterator;

use Iterator;
use Predis\ClientInterface;
use Predis\NotSupportedException;
use InvalidArgumentException;

/**
 * Provides the base implementation for a fully-rewindable PHP iterator that can
 * incrementally iterate over cursor-based collections stored on Redis using the
 * commands in the `SCAN` family.
 *
 * Given their incremental nature with multiple fetches, these kind of iterators
 * offer limited guarantees about the returned elements because the collection
 * can change several times during the iteration process.
 *
 * @see http://redis.io/commands/scan
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
abstract class CursorBasedIterator implements Iterator
{
    protected $client;
    protected $match;
    protected $count;

    protected $valid;
    protected $fetchmore;
    protected $elements;
    protected $cursor;
    protected $position;
    protected $current;

    /**
     * @param ClientInterface $client Client connected to Redis.
     * @param string          $match  Pattern to match during the server-side iteration.
     * @param int             $count  Hint used by Redis to compute the number of results per iteration.
     */
    public function __construct(ClientInterface $client, $match = null, $count = null)
    {
        $this->client = $client;
        $this->match = $match;
        $this->count = $count;

        $this->reset();
    }

    /**
     * Ensures that the client supports the specified Redis command required to
     * fetch elements from the server to perform the iteration.
     *
     * @param ClientInterface $client    Client connected to Redis.
     * @param string          $commandID Command ID.
     *
     * @throws NotSupportedException
     */
    protected function requiredCommand(ClientInterface $client, $commandID)
    {
        if (!$client->getProfile()->supportsCommand($commandID)) {
            throw new NotSupportedException("The current profile does not support '$commandID'.");
        }
    }

    /**
     * Resets the inner state of the iterator.
     */
    protected function reset()
    {
        $this->valid = true;
        $this->fetchmore = true;
        $this->elements = array();
        $this->cursor = 0;
        $this->position = -1;
        $this->current = null;
    }

    /**
     * Returns an array of options for the `SCAN` command.
     *
     * @return array
     */
    protected function getScanOptions()
    {
        $options = array();

        if (strlen($this->match) > 0) {
            $options['MATCH'] = $this->match;
        }

        if ($this->count > 0) {
            $options['COUNT'] = $this->count;
        }

        return $options;
    }

    /**
     * Fetches a new set of elements from the remote collection, effectively
     * advancing the iteration process.
     *
     * @return array
     */
    abstract protected function executeCommand();

    /**
     * Populates the local buffer of elements fetched from the server during
     * the iteration.
     */
    protected function fetch()
    {
		list($cursor, $elements) = $this->executeCommand();

        if (!$cursor) {
            $this->fetchmore = false;
        }

        $this->cursor = $cursor;
        $this->elements = $elements;
    }

    /**
     * Extracts next values for key() and current().
     */
    protected function extractNext()
    {
        $this->position++;
        $this->current = array_shift($this->elements);
    }

    /**
     * {@inheritdoc}
     */
    public function rewind()
    {
        $this->reset();
        $this->next();
    }

    /**
     * {@inheritdoc}
     */
    public function current()
    {
        return $this->current;
    }

    /**
     * {@inheritdoc}
     */
    public function key()
    {
        return $this->position;
    }

    /**
     * {@inheritdoc}
     */
    public function next()
    {
        tryFetch: {
            if (!$this->elements && $this->fetchmore) {
                $this->fetch();
            }

            if ($this->elements) {
                $this->extractNext();
            } elseif ($this->cursor) {
                goto tryFetch;
            } else {
                $this->valid = false;
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function valid()
    {
        return $this->valid;
    }
}

/**
 * Abstracts the iteration of members stored in a sorted set by leveraging the
 * ZSCAN command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 * @link http://redis.io/commands/scan
 */
class SortedSetKey extends CursorBasedIterator
{
    protected $key;

    /**
     * {@inheritdoc}
     */
    public function __construct(ClientInterface $client, $key, $match = null, $count = null)
    {
        $this->requiredCommand($client, 'ZSCAN');

        parent::__construct($client, $match, $count);

        $this->key = $key;
    }

    /**
     * {@inheritdoc}
     */
    protected function executeCommand()
    {
        return $this->client->zscan($this->key, $this->cursor, $this->getScanOptions());
    }

    /**
     * {@inheritdoc}
     */
    protected function extractNext()
    {
        if ($kv = each($this->elements)) {
            $this->position = $kv[0];
            $this->current = $kv[1];

            unset($this->elements[$this->position]);
        }
    }
}

/**
 * Abstracts the iteration of members stored in a set by leveraging the SSCAN
 * command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 * @link http://redis.io/commands/scan
 */
class SetKey extends CursorBasedIterator
{
    protected $key;

    /**
     * {@inheritdoc}
     */
    public function __construct(ClientInterface $client, $key, $match = null, $count = null)
    {
        $this->requiredCommand($client, 'SSCAN');

        parent::__construct($client, $match, $count);

        $this->key = $key;
    }

    /**
     * {@inheritdoc}
     */
    protected function executeCommand()
    {
        return $this->client->sscan($this->key, $this->cursor, $this->getScanOptions());
    }
}

/**
 * Abstracts the iteration of the keyspace on a Redis instance by leveraging the
 * SCAN command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 * @link http://redis.io/commands/scan
 */
class Keyspace extends CursorBasedIterator
{
    /**
     * {@inheritdoc}
     */
    public function __construct(ClientInterface $client, $match = null, $count = null)
    {
        $this->requiredCommand($client, 'SCAN');

        parent::__construct($client, $match, $count);
    }

    /**
     * {@inheritdoc}
     */
    protected function executeCommand()
    {
        return $this->client->scan($this->cursor, $this->getScanOptions());
    }
}

/**
 * Abstracts the iteration of fields and values of an hash by leveraging the
 * HSCAN command (Redis >= 2.8) wrapped in a fully-rewindable PHP iterator.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 * @link http://redis.io/commands/scan
 */
class HashKey extends CursorBasedIterator
{
    protected $key;

    /**
     * {@inheritdoc}
     */
    public function __construct(ClientInterface $client, $key, $match = null, $count = null)
    {
        $this->requiredCommand($client, 'HSCAN');

        parent::__construct($client, $match, $count);

        $this->key = $key;
    }

    /**
     * {@inheritdoc}
     */
    protected function executeCommand()
    {
        return $this->client->hscan($this->key, $this->cursor, $this->getScanOptions());
    }

    /**
     * {@inheritdoc}
     */
    protected function extractNext()
    {
        $this->position = key($this->elements);
        $this->current = array_shift($this->elements);
    }
}

/**
 * Abstracts the iteration of items stored in a list by leveraging the LRANGE
 * command wrapped in a fully-rewindable PHP iterator.
 *
 * This iterator tries to emulate the behaviour of cursor-based iterators based
 * on the SCAN-family of commands introduced in Redis <= 2.8, meaning that due
 * to its incremental nature with multiple fetches it can only offer limited
 * guarantees on the returned elements because the collection can change several
 * times (trimmed, deleted, overwritten) during the iteration process.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 * @link http://redis.io/commands/lrange
 */
class ListKey implements Iterator
{
    protected $client;
    protected $count;
    protected $key;

    protected $valid;
    protected $fetchmore;
    protected $elements;
    protected $position;
    protected $current;

    /**
     * @param ClientInterface $client Client connected to Redis.
     * @param string          $key    Redis list key.
     * @param int             $count  Number of items retrieved on each fetch operation.
     *
     * @throws \InvalidArgumentException
     */
    public function __construct(ClientInterface $client, $key, $count = 10)
    {
        $this->requiredCommand($client, 'LRANGE');

        if ((false === $count = filter_var($count, FILTER_VALIDATE_INT)) || $count < 0) {
            throw new InvalidArgumentException('The $count argument must be a positive integer.');
        }

        $this->client = $client;
        $this->key = $key;
        $this->count = $count;

        $this->reset();
    }

    /**
     * Ensures that the client instance supports the specified Redis command
     * required to fetch elements from the server to perform the iteration.
     *
     * @param ClientInterface $client    Client connected to Redis.
     * @param string          $commandID Command ID.
     *
     * @throws NotSupportedException
     */
    protected function requiredCommand(ClientInterface $client, $commandID)
    {
        if (!$client->getProfile()->supportsCommand($commandID)) {
            throw new NotSupportedException("The current profile does not support '$commandID'.");
        }
    }

    /**
     * Resets the inner state of the iterator.
     */
    protected function reset()
    {
        $this->valid = true;
        $this->fetchmore = true;
        $this->elements = array();
        $this->position = -1;
        $this->current = null;
    }

    /**
     * Fetches a new set of elements from the remote collection, effectively
     * advancing the iteration process.
     *
     * @return array
     */
    protected function executeCommand()
    {
        return $this->client->lrange($this->key, $this->position + 1, $this->position + $this->count);
    }

    /**
     * Populates the local buffer of elements fetched from the server during the
     * iteration.
     */
    protected function fetch()
    {
        $elements = $this->executeCommand();

        if (count($elements) < $this->count) {
            $this->fetchmore = false;
        }

        $this->elements = $elements;
    }

    /**
     * Extracts next values for key() and current().
     */
    protected function extractNext()
    {
        $this->position++;
        $this->current = array_shift($this->elements);
    }

    /**
     * {@inheritdoc}
     */
    public function rewind()
    {
        $this->reset();
        $this->next();
    }

    /**
     * {@inheritdoc}
     */
    public function current()
    {
        return $this->current;
    }

    /**
     * {@inheritdoc}
     */
    public function key()
    {
        return $this->position;
    }

    /**
     * {@inheritdoc}
     */
    public function next()
    {
        if (!$this->elements && $this->fetchmore) {
            $this->fetch();
        }

        if ($this->elements) {
            $this->extractNext();
        } else {
            $this->valid = false;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function valid()
    {
        return $this->valid;
    }
}

/* --------------------------------------------------------------------------- */

namespace Predis\Cluster;

use InvalidArgumentException;
use Predis\Command\CommandInterface;
use Predis\Command\ScriptCommand;
use Predis\Cluster\Distributor\DistributorInterface;
use Predis\Cluster\Distributor\HashRing;
use Predis\NotSupportedException;
use Predis\Cluster\Hash\HashGeneratorInterface;
use Predis\Cluster\Hash\CRC16;

/**
 * Interface for classes defining the strategy used to calculate an hash out of
 * keys extracted from supported commands.
 *
 * This is mostly useful to support clustering via client-side sharding.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface StrategyInterface
{
    /**
     * Returns a slot for the given command used for clustering distribution or
     * NULL when this is not possible.
     *
     * @param CommandInterface $command Command instance.
     *
     * @return int
     */
    public function getSlot(CommandInterface $command);

    /**
     * Returns a slot for the given key used for clustering distribution or NULL
     * when this is not possible.
     *
     * @param string $key Key string.
     *
     * @return int
     */
    public function getSlotByKey($key);

    /**
     * Returns a distributor instance to be used by the cluster.
     *
     * @return DistributorInterface
     */
    public function getDistributor();
}

/**
 * Common class implementing the logic needed to support clustering strategies.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
abstract class ClusterStrategy implements StrategyInterface
{
    protected $commands;

    /**
     *
     */
    public function __construct()
    {
        $this->commands = $this->getDefaultCommands();
    }

    /**
     * Returns the default map of supported commands with their handlers.
     *
     * @return array
     */
    protected function getDefaultCommands()
    {
        $getKeyFromFirstArgument = array($this, 'getKeyFromFirstArgument');
        $getKeyFromAllArguments = array($this, 'getKeyFromAllArguments');

        return array(
            /* commands operating on the key space */
            'EXISTS'                => $getKeyFromFirstArgument,
            'DEL'                   => $getKeyFromAllArguments,
            'TYPE'                  => $getKeyFromFirstArgument,
            'EXPIRE'                => $getKeyFromFirstArgument,
            'EXPIREAT'              => $getKeyFromFirstArgument,
            'PERSIST'               => $getKeyFromFirstArgument,
            'PEXPIRE'               => $getKeyFromFirstArgument,
            'PEXPIREAT'             => $getKeyFromFirstArgument,
            'TTL'                   => $getKeyFromFirstArgument,
            'PTTL'                  => $getKeyFromFirstArgument,
            'SORT'                  => $getKeyFromFirstArgument, // TODO
            'DUMP'                  => $getKeyFromFirstArgument,
            'RESTORE'               => $getKeyFromFirstArgument,

            /* commands operating on string values */
            'APPEND'                => $getKeyFromFirstArgument,
            'DECR'                  => $getKeyFromFirstArgument,
            'DECRBY'                => $getKeyFromFirstArgument,
            'GET'                   => $getKeyFromFirstArgument,
            'GETBIT'                => $getKeyFromFirstArgument,
            'MGET'                  => $getKeyFromAllArguments,
            'SET'                   => $getKeyFromFirstArgument,
            'GETRANGE'              => $getKeyFromFirstArgument,
            'GETSET'                => $getKeyFromFirstArgument,
            'INCR'                  => $getKeyFromFirstArgument,
            'INCRBY'                => $getKeyFromFirstArgument,
            'INCRBYFLOAT'           => $getKeyFromFirstArgument,
            'SETBIT'                => $getKeyFromFirstArgument,
            'SETEX'                 => $getKeyFromFirstArgument,
            'MSET'                  => array($this, 'getKeyFromInterleavedArguments'),
            'MSETNX'                => array($this, 'getKeyFromInterleavedArguments'),
            'SETNX'                 => $getKeyFromFirstArgument,
            'SETRANGE'              => $getKeyFromFirstArgument,
            'STRLEN'                => $getKeyFromFirstArgument,
            'SUBSTR'                => $getKeyFromFirstArgument,
            'BITOP'                 => array($this, 'getKeyFromBitOp'),
            'BITCOUNT'              => $getKeyFromFirstArgument,

            /* commands operating on lists */
            'LINSERT'               => $getKeyFromFirstArgument,
            'LINDEX'                => $getKeyFromFirstArgument,
            'LLEN'                  => $getKeyFromFirstArgument,
            'LPOP'                  => $getKeyFromFirstArgument,
            'RPOP'                  => $getKeyFromFirstArgument,
            'RPOPLPUSH'             => $getKeyFromAllArguments,
            'BLPOP'                 => array($this, 'getKeyFromBlockingListCommands'),
            'BRPOP'                 => array($this, 'getKeyFromBlockingListCommands'),
            'BRPOPLPUSH'            => array($this, 'getKeyFromBlockingListCommands'),
            'LPUSH'                 => $getKeyFromFirstArgument,
            'LPUSHX'                => $getKeyFromFirstArgument,
            'RPUSH'                 => $getKeyFromFirstArgument,
            'RPUSHX'                => $getKeyFromFirstArgument,
            'LRANGE'                => $getKeyFromFirstArgument,
            'LREM'                  => $getKeyFromFirstArgument,
            'LSET'                  => $getKeyFromFirstArgument,
            'LTRIM'                 => $getKeyFromFirstArgument,

            /* commands operating on sets */
            'SADD'                  => $getKeyFromFirstArgument,
            'SCARD'                 => $getKeyFromFirstArgument,
            'SDIFF'                 => $getKeyFromAllArguments,
            'SDIFFSTORE'            => $getKeyFromAllArguments,
            'SINTER'                => $getKeyFromAllArguments,
            'SINTERSTORE'           => $getKeyFromAllArguments,
            'SUNION'                => $getKeyFromAllArguments,
            'SUNIONSTORE'           => $getKeyFromAllArguments,
            'SISMEMBER'             => $getKeyFromFirstArgument,
            'SMEMBERS'              => $getKeyFromFirstArgument,
            'SSCAN'                 => $getKeyFromFirstArgument,
            'SPOP'                  => $getKeyFromFirstArgument,
            'SRANDMEMBER'           => $getKeyFromFirstArgument,
            'SREM'                  => $getKeyFromFirstArgument,

            /* commands operating on sorted sets */
            'ZADD'                  => $getKeyFromFirstArgument,
            'ZCARD'                 => $getKeyFromFirstArgument,
            'ZCOUNT'                => $getKeyFromFirstArgument,
            'ZINCRBY'               => $getKeyFromFirstArgument,
            'ZINTERSTORE'           => array($this, 'getKeyFromZsetAggregationCommands'),
            'ZRANGE'                => $getKeyFromFirstArgument,
            'ZRANGEBYSCORE'         => $getKeyFromFirstArgument,
            'ZRANK'                 => $getKeyFromFirstArgument,
            'ZREM'                  => $getKeyFromFirstArgument,
            'ZREMRANGEBYRANK'       => $getKeyFromFirstArgument,
            'ZREMRANGEBYSCORE'      => $getKeyFromFirstArgument,
            'ZREVRANGE'             => $getKeyFromFirstArgument,
            'ZREVRANGEBYSCORE'      => $getKeyFromFirstArgument,
            'ZREVRANK'              => $getKeyFromFirstArgument,
            'ZSCORE'                => $getKeyFromFirstArgument,
            'ZUNIONSTORE'           => array($this, 'getKeyFromZsetAggregationCommands'),
            'ZSCAN'                 => $getKeyFromFirstArgument,
            'ZLEXCOUNT'             => $getKeyFromFirstArgument,
            'ZRANGEBYLEX'           => $getKeyFromFirstArgument,
            'ZREMRANGEBYLEX'        => $getKeyFromFirstArgument,

            /* commands operating on hashes */
            'HDEL'                  => $getKeyFromFirstArgument,
            'HEXISTS'               => $getKeyFromFirstArgument,
            'HGET'                  => $getKeyFromFirstArgument,
            'HGETALL'               => $getKeyFromFirstArgument,
            'HMGET'                 => $getKeyFromFirstArgument,
            'HMSET'                 => $getKeyFromFirstArgument,
            'HINCRBY'               => $getKeyFromFirstArgument,
            'HINCRBYFLOAT'          => $getKeyFromFirstArgument,
            'HKEYS'                 => $getKeyFromFirstArgument,
            'HLEN'                  => $getKeyFromFirstArgument,
            'HSET'                  => $getKeyFromFirstArgument,
            'HSETNX'                => $getKeyFromFirstArgument,
            'HVALS'                 => $getKeyFromFirstArgument,
            'HSCAN'                 => $getKeyFromFirstArgument,

            /* commands operating on HyperLogLog */
            'PFADD'                 => $getKeyFromFirstArgument,
            'PFCOUNT'               => $getKeyFromAllArguments,
            'PFMERGE'               => $getKeyFromAllArguments,

            /* scripting */
            'EVAL'                  => array($this, 'getKeyFromScriptingCommands'),
            'EVALSHA'               => array($this, 'getKeyFromScriptingCommands'),
        );
    }

    /**
     * Returns the list of IDs for the supported commands.
     *
     * @return array
     */
    public function getSupportedCommands()
    {
        return array_keys($this->commands);
    }

    /**
     * Sets an handler for the specified command ID.
     *
     * The signature of the callback must have a single parameter of type
     * Predis\Command\CommandInterface.
     *
     * When the callback argument is omitted or NULL, the previously associated
     * handler for the specified command ID is removed.
     *
     * @param string $commandID Command ID.
     * @param mixed  $callback  A valid callable object, or NULL to unset the handler.
     *
     * @throws \InvalidArgumentException
     */
    public function setCommandHandler($commandID, $callback = null)
    {
        $commandID = strtoupper($commandID);

        if (!isset($callback)) {
            unset($this->commands[$commandID]);

            return;
        }

        if (!is_callable($callback)) {
            throw new InvalidArgumentException(
                "The argument must be a callable object or NULL."
            );
        }

        $this->commands[$commandID] = $callback;
    }

    /**
     * Extracts the key from the first argument of a command instance.
     *
     * @param CommandInterface $command Command instance.
     *
     * @return string
     */
    protected function getKeyFromFirstArgument(CommandInterface $command)
    {
        return $command->getArgument(0);
    }

    /**
     * Extracts the key from a command with multiple keys only when all keys in
     * the arguments array produce the same hash.
     *
     * @param CommandInterface $command Command instance.
     *
     * @return string|null
     */
    protected function getKeyFromAllArguments(CommandInterface $command)
    {
        $arguments = $command->getArguments();

        if ($this->checkSameSlotForKeys($arguments)) {
            return $arguments[0];
        }
    }

    /**
     * Extracts the key from a command with multiple keys only when all keys in
     * the arguments array produce the same hash.
     *
     * @param CommandInterface $command Command instance.
     *
     * @return string|null
     */
    protected function getKeyFromInterleavedArguments(CommandInterface $command)
    {
        $arguments = $command->getArguments();
        $keys = array();

        for ($i = 0; $i < count($arguments); $i += 2) {
            $keys[] = $arguments[$i];
        }

        if ($this->checkSameSlotForKeys($keys)) {
            return $arguments[0];
        }
    }

    /**
     * Extracts the key from BLPOP and BRPOP commands.
     *
     * @param CommandInterface $command Command instance.
     *
     * @return string|null
     */
    protected function getKeyFromBlockingListCommands(CommandInterface $command)
    {
        $arguments = $command->getArguments();

        if ($this->checkSameSlotForKeys(array_slice($arguments, 0, count($arguments) - 1))) {
            return $arguments[0];
        }
    }

    /**
     * Extracts the key from BITOP command.
     *
     * @param CommandInterface $command Command instance.
     *
     * @return string|null
     */
    protected function getKeyFromBitOp(CommandInterface $command)
    {
        $arguments = $command->getArguments();

        if ($this->checkSameSlotForKeys(array_slice($arguments, 1, count($arguments)))) {
            return $arguments[1];
        }
    }

    /**
     * Extracts the key from ZINTERSTORE and ZUNIONSTORE commands.
     *
     * @param CommandInterface $command Command instance.
     *
     * @return string|null
     */
    protected function getKeyFromZsetAggregationCommands(CommandInterface $command)
    {
        $arguments = $command->getArguments();
        $keys = array_merge(array($arguments[0]), array_slice($arguments, 2, $arguments[1]));

        if ($this->checkSameSlotForKeys($keys)) {
            return $arguments[0];
        }
    }

    /**
     * Extracts the key from EVAL and EVALSHA commands.
     *
     * @param CommandInterface $command Command instance.
     *
     * @return string|null
     */
    protected function getKeyFromScriptingCommands(CommandInterface $command)
    {
        if ($command instanceof ScriptCommand) {
            $keys = $command->getKeys();
        } else {
            $keys = array_slice($args = $command->getArguments(), 2, $args[1]);
        }

        if ($keys && $this->checkSameSlotForKeys($keys)) {
            return $keys[0];
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getSlot(CommandInterface $command)
    {
        $slot = $command->getSlot();

        if (!isset($slot) && isset($this->commands[$cmdID = $command->getId()])) {
            $key = call_user_func($this->commands[$cmdID], $command);

            if (isset($key)) {
                $slot = $this->getSlotByKey($key);
                $command->setSlot($slot);
            }
        }

        return $slot;
    }

    /**
     * Checks if the specified array of keys will generate the same hash.
     *
     * @param array $keys Array of keys.
     *
     * @return bool
     */
    protected function checkSameSlotForKeys(array $keys)
    {
        if (!$count = count($keys)) {
            return false;
        }

        $currentSlot = $this->getSlotByKey($keys[0]);

        for ($i = 1; $i < $count; $i++) {
            $nextSlot = $this->getSlotByKey($keys[$i]);

            if ($currentSlot !== $nextSlot) {
                return false;
            }

            $currentSlot = $nextSlot;
        }

        return true;
    }

    /**
     * Returns only the hashable part of a key (delimited by "{...}"), or the
     * whole key if a key tag is not found in the string.
     *
     * @param string $key A key.
     *
     * @return string
     */
    protected function extractKeyTag($key)
    {
        if (false !== $start = strpos($key, '{')) {
            if (false !== ($end = strpos($key, '}', $start)) && $end !== ++$start) {
                $key = substr($key, $start, $end - $start);
            }
        }

        return $key;
    }
}

/**
 * Default cluster strategy used by Predis to handle client-side sharding.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class PredisStrategy extends ClusterStrategy
{
    protected $distributor;

    /**
     * @param DistributorInterface $distributor Optional distributor instance.
     */
    public function __construct(DistributorInterface $distributor = null)
    {
        parent::__construct();

        $this->distributor = $distributor ?: new HashRing();
    }

    /**
     * {@inheritdoc}
     */
    public function getSlotByKey($key)
    {
        $key = $this->extractKeyTag($key);
        $hash = $this->distributor->hash($key);
        $slot = $this->distributor->getSlot($hash);

        return $slot;
    }

    /**
     * {@inheritdoc}
     */
    protected function checkSameSlotForKeys(array $keys)
    {
        if (!$count = count($keys)) {
            return false;
        }

        $currentKey = $this->extractKeyTag($keys[0]);

        for ($i = 1; $i < $count; $i++) {
            $nextKey = $this->extractKeyTag($keys[$i]);

            if ($currentKey !== $nextKey) {
                return false;
            }

            $currentKey = $nextKey;
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function getDistributor()
    {
        return $this->distributor;
    }
}

/**
 * Default class used by Predis to calculate hashes out of keys of
 * commands supported by redis-cluster.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class RedisStrategy extends ClusterStrategy
{
    protected $hashGenerator;

    /**
     * @param HashGeneratorInterface $hashGenerator Hash generator instance.
     */
    public function __construct(HashGeneratorInterface $hashGenerator = null)
    {
        parent::__construct();

        $this->hashGenerator = $hashGenerator ?: new CRC16();
    }

    /**
     * {@inheritdoc}
     */
    public function getSlotByKey($key)
    {
        $key  = $this->extractKeyTag($key);
        $slot = $this->hashGenerator->hash($key) & 0x3FFF;

        return $slot;
    }

    /**
     * {@inheritdoc}
     */
    public function getDistributor()
    {
        throw new NotSupportedException(
            'This cluster strategy does not provide an external distributor'
        );
    }
}

/* --------------------------------------------------------------------------- */

namespace Predis\Protocol;

use Predis\CommunicationException;
use Predis\Command\CommandInterface;
use Predis\Connection\CompositeConnectionInterface;

/**
 * Defines a pluggable protocol processor capable of serializing commands and
 * deserializing responses into PHP objects directly from a connection.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface ProtocolProcessorInterface
{
    /**
     * Writes a request over a connection to Redis.
     *
     * @param CompositeConnectionInterface $connection Redis connection.
     * @param CommandInterface             $command    Command instance.
     */
    public function write(CompositeConnectionInterface $connection, CommandInterface $command);

    /**
     * Reads a response from a connection to Redis.
     *
     * @param CompositeConnectionInterface $connection Redis connection.
     *
     * @return mixed
     */
    public function read(CompositeConnectionInterface $connection);
}

/**
 * Defines a pluggable reader capable of parsing responses returned by Redis and
 * deserializing them to PHP objects.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface ResponseReaderInterface
{
    /**
     * Reads a response from a connection to Redis.
     *
     * @param CompositeConnectionInterface $connection Redis connection.
     *
     * @return mixed
     */
    public function read(CompositeConnectionInterface $connection);
}

/**
 * Defines a pluggable serializer for Redis commands.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface RequestSerializerInterface
{
    /**
     * Serializes a Redis command.
     *
     * @param CommandInterface $command Redis command.
     *
     * @return string
     */
    public function serialize(CommandInterface $command);
}

/**
 * Exception used to indentify errors encountered while parsing the Redis wire
 * protocol.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ProtocolException extends CommunicationException
{
}

/* --------------------------------------------------------------------------- */

namespace Predis\Connection\Aggregate;

use Predis\Connection\AggregateConnectionInterface;
use InvalidArgumentException;
use RuntimeException;
use Predis\Command\CommandInterface;
use Predis\Connection\NodeConnectionInterface;
use Predis\Replication\ReplicationStrategy;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use Predis\NotSupportedException;
use Predis\Cluster\PredisStrategy;
use Predis\Cluster\StrategyInterface;
use OutOfBoundsException;
use Predis\Cluster\RedisStrategy as RedisClusterStrategy;
use Predis\Command\RawCommand;
use Predis\Connection\FactoryInterface;
use Predis\Response\ErrorInterface as ErrorResponseInterface;

/**
 * Defines a cluster of Redis servers formed by aggregating multiple connection
 * instances to single Redis nodes.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface ClusterInterface extends AggregateConnectionInterface
{
}

/**
 * Defines a group of Redis nodes in a master / slave replication setup.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface ReplicationInterface extends AggregateConnectionInterface
{
    /**
     * Switches the internal connection instance in use.
     *
     * @param string $connection Alias of a connection
     */
    public function switchTo($connection);

    /**
     * Returns the connection instance currently in use by the aggregate
     * connection.
     *
     * @return NodeConnectionInterface
     */
    public function getCurrent();

    /**
     * Returns the connection instance for the master Redis node.
     *
     * @return NodeConnectionInterface
     */
    public function getMaster();

    /**
     * Returns a list of connection instances to slave nodes.
     *
     * @return NodeConnectionInterface
     */
    public function getSlaves();
}

/**
 * Abstraction for a Redis-backed cluster of nodes (Redis >= 3.0.0).
 *
 * This connection backend offers smart support for redis-cluster by handling
 * automatic slots map (re)generation upon -MOVED or -ASK responses returned by
 * Redis when redirecting a client to a different node.
 *
 * The cluster can be pre-initialized using only a subset of the actual nodes in
 * the cluster, Predis will do the rest by adjusting the slots map and creating
 * the missing underlying connection instances on the fly.
 *
 * It is possible to pre-associate connections to a slots range with the "slots"
 * parameter in the form "$first-$last". This can greatly reduce runtime node
 * guessing and redirections.
 *
 * It is also possible to ask for the full and updated slots map directly to one
 * of the nodes and optionally enable such a behaviour upon -MOVED redirections.
 * Asking for the cluster configuration to Redis is actually done by issuing a
 * CLUSTER SLOTS command to a random node in the pool.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class RedisCluster implements ClusterInterface, IteratorAggregate, Countable
{
    private $useClusterSlots = true;
    private $defaultParameters = array();
    private $pool = array();
    private $slots = array();
    private $slotsMap;
    private $strategy;
    private $connections;

    /**
     * @param FactoryInterface  $connections Optional connection factory.
     * @param StrategyInterface $strategy    Optional cluster strategy.
     */
    public function __construct(
        FactoryInterface $connections,
        StrategyInterface $strategy = null
    ) {
        $this->connections = $connections;
        $this->strategy = $strategy ?: new RedisClusterStrategy();
    }

    /**
     * {@inheritdoc}
     */
    public function isConnected()
    {
        foreach ($this->pool as $connection) {
            if ($connection->isConnected()) {
                return true;
            }
        }

        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function connect()
    {
        if ($connection = $this->getRandomConnection()) {
            $connection->connect();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function disconnect()
    {
        foreach ($this->pool as $connection) {
            $connection->disconnect();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function add(NodeConnectionInterface $connection)
    {
        $this->pool[(string) $connection] = $connection;
        unset($this->slotsMap);
    }

    /**
     * {@inheritdoc}
     */
    public function remove(NodeConnectionInterface $connection)
    {
        if (false !== $id = array_search($connection, $this->pool, true)) {
            unset(
                $this->pool[$id],
                $this->slotsMap
            );

            return true;
        }

        return false;
    }

    /**
     * Removes a connection instance by using its identifier.
     *
     * @param string $connectionID Connection identifier.
     *
     * @return bool True if the connection was in the pool.
     */
    public function removeById($connectionID)
    {
        if (isset($this->pool[$connectionID])) {
            unset(
                $this->pool[$connectionID],
                $this->slotsMap
            );

            return true;
        }

        return false;
    }

    /**
     * Generates the current slots map by guessing the cluster configuration out
     * of the connection parameters of the connections in the pool.
     *
     * Generation is based on the same algorithm used by Redis to generate the
     * cluster, so it is most effective when all of the connections supplied on
     * initialization have the "slots" parameter properly set accordingly to the
     * current cluster configuration.
     */
    public function buildSlotsMap()
    {
        $this->slotsMap = array();

        foreach ($this->pool as $connectionID => $connection) {
            $parameters = $connection->getParameters();

            if (!isset($parameters->slots)) {
                continue;
            }

            $slots = explode('-', $parameters->slots, 2);
            $this->setSlots($slots[0], $slots[1], $connectionID);
        }
    }

    /**
     * Generates an updated slots map fetching the cluster configuration using
     * the CLUSTER SLOTS command against the specified node or a random one from
     * the pool.
     *
     * @param NodeConnectionInterface $connection Optional connection instance.
     *
     * @return array
     */
    public function askSlotsMap(NodeConnectionInterface $connection = null)
    {
        if (!$connection && !$connection = $this->getRandomConnection()) {
            return array();
        }
        $command = RawCommand::create('CLUSTER', 'SLOTS');
        $response = $connection->executeCommand($command);

        foreach ($response as $slots) {
            // We only support master servers for now, so we ignore subsequent
            // elements in the $slots array identifying slaves.
            list($start, $end, $master) = $slots;

            if ($master[0] === '') {
                $this->setSlots($start, $end, (string) $connection);
            } else {
                $this->setSlots($start, $end, "{$master[0]}:{$master[1]}");
            }
        }

        return $this->slotsMap;
    }

    /**
     * Returns the current slots map for the cluster.
     *
     * @return array
     */
    public function getSlotsMap()
    {
        if (!isset($this->slotsMap)) {
            $this->slotsMap = array();
        }

        return $this->slotsMap;
    }

    /**
     * Pre-associates a connection to a slots range to avoid runtime guessing.
     *
     * @param int                            $first      Initial slot of the range.
     * @param int                            $last       Last slot of the range.
     * @param NodeConnectionInterface|string $connection ID or connection instance.
     *
     * @throws \OutOfBoundsException
     */
    public function setSlots($first, $last, $connection)
    {
        if ($first < 0x0000 || $first > 0x3FFF ||
            $last < 0x0000 || $last > 0x3FFF ||
            $last < $first
        ) {
            throw new OutOfBoundsException(
                "Invalid slot range for $connection: [$first-$last]."
            );
        }

        $slots = array_fill($first, $last - $first + 1, (string) $connection);
        $this->slotsMap = $this->getSlotsMap() + $slots;
    }

    /**
     * Guesses the correct node associated to a given slot using a precalculated
     * slots map, falling back to the same logic used by Redis to initialize a
     * cluster (best-effort).
     *
     * @param int $slot Slot index.
     *
     * @return string Connection ID.
     */
    protected function guessNode($slot)
    {
        if (!isset($this->slotsMap)) {
            $this->buildSlotsMap();
        }

        if (isset($this->slotsMap[$slot])) {
            return $this->slotsMap[$slot];
        }

        $count = count($this->pool);
        $index = min((int) ($slot / (int) (16384 / $count)), $count - 1);
        $nodes = array_keys($this->pool);

        return $nodes[$index];
    }

    /**
     * Creates a new connection instance from the given connection ID.
     *
     * @param string $connectionID Identifier for the connection.
     *
     * @return NodeConnectionInterface
     */
    protected function createConnection($connectionID)
    {
        $host = explode(':', $connectionID, 2);

        $parameters = array_merge($this->defaultParameters, array(
            'host' => $host[0],
            'port' => $host[1],
        ));

        $connection = $this->connections->create($parameters);

        return $connection;
    }

    /**
     * {@inheritdoc}
     */
    public function getConnection(CommandInterface $command)
    {
        $slot = $this->strategy->getSlot($command);

        if (!isset($slot)) {
            throw new NotSupportedException(
                "Cannot use '{$command->getId()}' with redis-cluster."
            );
        }

        if (isset($this->slots[$slot])) {
            return $this->slots[$slot];
        } else {
            return $this->getConnectionBySlot($slot);
        }
    }

    /**
     * Returns the connection currently associated to a given slot.
     *
     * @param int $slot Slot index.
     *
     * @return NodeConnectionInterface
     *
     * @throws \OutOfBoundsException
     */
    public function getConnectionBySlot($slot)
    {
        if ($slot < 0x0000 || $slot > 0x3FFF) {
            throw new OutOfBoundsException("Invalid slot [$slot].");
        }

        if (isset($this->slots[$slot])) {
            return $this->slots[$slot];
        }

        $connectionID = $this->guessNode($slot);

        if (!$connection = $this->getConnectionById($connectionID)) {
            $connection = $this->createConnection($connectionID);
            $this->pool[$connectionID] = $connection;
        }

        return $this->slots[$slot] = $connection;
    }

    /**
     * {@inheritdoc}
     */
    public function getConnectionById($connectionID)
    {
        if (isset($this->pool[$connectionID])) {
            return $this->pool[$connectionID];
        }
    }

    /**
     * Returns a random connection from the pool.
     *
     * @return NodeConnectionInterface|null
     */
    protected function getRandomConnection()
    {
        if ($this->pool) {
            return $this->pool[array_rand($this->pool)];
        }
    }

    /**
     * Permanently associates the connection instance to a new slot.
     * The connection is added to the connections pool if not yet included.
     *
     * @param NodeConnectionInterface $connection Connection instance.
     * @param int                     $slot       Target slot index.
     */
    protected function move(NodeConnectionInterface $connection, $slot)
    {
        $this->pool[(string) $connection] = $connection;
        $this->slots[(int) $slot] = $connection;
    }

    /**
     * Handles -ERR responses returned by Redis.
     *
     * @param CommandInterface       $command Command that generated the -ERR response.
     * @param ErrorResponseInterface $error   Redis error response object.
     *
     * @return mixed
     */
    protected function onErrorResponse(CommandInterface $command, ErrorResponseInterface $error)
    {
        $details = explode(' ', $error->getMessage(), 2);

        switch ($details[0]) {
            case 'MOVED':
                return $this->onMovedResponse($command, $details[1]);

            case 'ASK':
                return $this->onAskResponse($command, $details[1]);

            default:
                return $error;
        }
    }

    /**
     * Handles -MOVED responses by executing again the command against the node
     * indicated by the Redis response.
     *
     * @param CommandInterface $command Command that generated the -MOVED response.
     * @param string           $details Parameters of the -MOVED response.
     *
     * @return mixed
     */
    protected function onMovedResponse(CommandInterface $command, $details)
    {
        list($slot, $connectionID) = explode(' ', $details, 2);

        if (!$connection = $this->getConnectionById($connectionID)) {
            $connection = $this->createConnection($connectionID);
        }

        if ($this->useClusterSlots) {
            $this->askSlotsMap($connection);
        }

        $this->move($connection, $slot);
        $response = $this->executeCommand($command);

        return $response;
    }

    /**
     * Handles -ASK responses by executing again the command against the node
     * indicated by the Redis response.
     *
     * @param  CommandInterface $command Command that generated the -ASK response.
     * @param  string           $details Parameters of the -ASK response.
     * @return mixed
     */
    protected function onAskResponse(CommandInterface $command, $details)
    {
        list($slot, $connectionID) = explode(' ', $details, 2);

        if (!$connection = $this->getConnectionById($connectionID)) {
            $connection = $this->createConnection($connectionID);
        }
        $connection->executeCommand(RawCommand::create('ASKING'));
        $response = $connection->executeCommand($command);

        return $response;
    }

    /**
     * {@inheritdoc}
     */
    public function writeRequest(CommandInterface $command)
    {
        $this->getConnection($command)->writeRequest($command);
    }

    /**
     * {@inheritdoc}
     */
    public function readResponse(CommandInterface $command)
    {
        return $this->getConnection($command)->readResponse($command);
    }

    /**
     * {@inheritdoc}
     */
    public function executeCommand(CommandInterface $command)
    {
        $connection = $this->getConnection($command);
        $response = $connection->executeCommand($command);

        if ($response instanceof ErrorResponseInterface) {
            return $this->onErrorResponse($command, $response);
        }

        return $response;
    }

    /**
     * {@inheritdoc}
     */
    public function count()
    {
        return count($this->pool);
    }

    /**
     * {@inheritdoc}
     */
    public function getIterator()
    {
        return new ArrayIterator(array_values($this->pool));
    }

    /**
     * Returns the underlying command hash strategy used to hash commands by
     * using keys found in their arguments.
     *
     * @return StrategyInterface
     */
    public function getClusterStrategy()
    {
        return $this->strategy;
    }

    /**
     * Returns the underlying connection factory used to create new connection
     * instances to Redis nodes indicated by redis-cluster.
     *
     * @return FactoryInterface
     */
    public function getConnectionFactory()
    {
        return $this->connections;
    }

    /**
     * Enables automatic fetching of the current slots map from one of the nodes
     * using the CLUSTER SLOTS command. This option is disabled by default but
     * asking the current slots map to Redis upon -MOVED responses may reduce
     * overhead by eliminating the trial-and-error nature of the node guessing
     * procedure, mostly when targeting many keys that would end up in a lot of
     * redirections.
     *
     * The slots map can still be manually fetched using the askSlotsMap()
     * method whether or not this option is enabled.
     *
     * @param bool $value Enable or disable the use of CLUSTER SLOTS.
     */
    public function useClusterSlots($value)
    {
        $this->useClusterSlots = (bool) $value;
    }

    /**
     * Sets a default array of connection parameters to be applied when creating
     * new connection instances on the fly when they are not part of the initial
     * pool supplied upon cluster initialization.
     *
     * These parameters are not applied to connections added to the pool using
     * the add() method.
     *
     * @param array $parameters Array of connection parameters.
     */
    public function setDefaultParameters(array $parameters)
    {
        $this->defaultParameters = array_merge(
            $this->defaultParameters,
            $parameters ?: array()
        );
    }
}

/**
 * Abstraction for a cluster of aggregate connections to various Redis servers
 * implementing client-side sharding based on pluggable distribution strategies.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 * @todo Add the ability to remove connections from pool.
 */
class PredisCluster implements ClusterInterface, IteratorAggregate, Countable
{
    private $pool;
    private $strategy;
    private $distributor;

    /**
     * @param StrategyInterface $strategy Optional cluster strategy.
     */
    public function __construct(StrategyInterface $strategy = null)
    {
        $this->pool = array();
        $this->strategy = $strategy ?: new PredisStrategy();
        $this->distributor = $this->strategy->getDistributor();
    }

    /**
     * {@inheritdoc}
     */
    public function isConnected()
    {
        foreach ($this->pool as $connection) {
            if ($connection->isConnected()) {
                return true;
            }
        }

        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function connect()
    {
        foreach ($this->pool as $connection) {
            $connection->connect();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function disconnect()
    {
        foreach ($this->pool as $connection) {
            $connection->disconnect();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function add(NodeConnectionInterface $connection)
    {
        $parameters = $connection->getParameters();

        if (isset($parameters->alias)) {
            $this->pool[$parameters->alias] = $connection;
        } else {
            $this->pool[] = $connection;
        }

        $weight = isset($parameters->weight) ? $parameters->weight : null;
        $this->distributor->add($connection, $weight);
    }

    /**
     * {@inheritdoc}
     */
    public function remove(NodeConnectionInterface $connection)
    {
        if (($id = array_search($connection, $this->pool, true)) !== false) {
            unset($this->pool[$id]);
            $this->distributor->remove($connection);

            return true;
        }

        return false;
    }

    /**
     * Removes a connection instance using its alias or index.
     *
     * @param string $connectionID Alias or index of a connection.
     *
     * @return bool Returns true if the connection was in the pool.
     */
    public function removeById($connectionID)
    {
        if ($connection = $this->getConnectionById($connectionID)) {
            return $this->remove($connection);
        }

        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function getConnection(CommandInterface $command)
    {
        $slot = $this->strategy->getSlot($command);

        if (!isset($slot)) {
            throw new NotSupportedException(
                "Cannot use '{$command->getId()}' over clusters of connections."
            );
        }

        $node = $this->distributor->getBySlot($slot);

        return $node;
    }

    /**
     * {@inheritdoc}
     */
    public function getConnectionById($connectionID)
    {
        return isset($this->pool[$connectionID]) ? $this->pool[$connectionID] : null;
    }

    /**
     * Retrieves a connection instance from the cluster using a key.
     *
     * @param string $key Key string.
     *
     * @return NodeConnectionInterface
     */
    public function getConnectionByKey($key)
    {
        $hash = $this->strategy->getSlotByKey($key);
        $node = $this->distributor->getBySlot($hash);

        return $node;
    }

    /**
     * Returns the underlying command hash strategy used to hash commands by
     * using keys found in their arguments.
     *
     * @return StrategyInterface
     */
    public function getClusterStrategy()
    {
        return $this->strategy;
    }

    /**
     * {@inheritdoc}
     */
    public function count()
    {
        return count($this->pool);
    }

    /**
     * {@inheritdoc}
     */
    public function getIterator()
    {
        return new ArrayIterator($this->pool);
    }

    /**
     * {@inheritdoc}
     */
    public function writeRequest(CommandInterface $command)
    {
        $this->getConnection($command)->writeRequest($command);
    }

    /**
     * {@inheritdoc}
     */
    public function readResponse(CommandInterface $command)
    {
        return $this->getConnection($command)->readResponse($command);
    }

    /**
     * {@inheritdoc}
     */
    public function executeCommand(CommandInterface $command)
    {
        return $this->getConnection($command)->executeCommand($command);
    }

    /**
     * Executes the specified Redis command on all the nodes of a cluster.
     *
     * @param CommandInterface $command A Redis command.
     *
     * @return array
     */
    public function executeCommandOnNodes(CommandInterface $command)
    {
        $responses = array();
        foreach ($this->pool as $connection) {
            $responses[] = $connection->executeCommand($command);
        }

        return $responses;
    }
}

/**
 * Aggregate connection handling replication of Redis nodes configured in a
 * single master / multiple slaves setup.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class MasterSlaveReplication implements ReplicationInterface
{
    protected $strategy;
    protected $master;
    protected $slaves;
    protected $current;

    /**
     * {@inheritdoc}
     */
    public function __construct(ReplicationStrategy $strategy = null)
    {
        $this->slaves = array();
        $this->strategy = $strategy ?: new ReplicationStrategy();
    }

    /**
     * Checks if one master and at least one slave have been defined.
     */
    protected function check()
    {
        if (!isset($this->master) || !$this->slaves) {
            throw new RuntimeException('Replication needs one master and at least one slave.');
        }
    }

    /**
     * Resets the connection state.
     */
    protected function reset()
    {
        $this->current = null;
    }

    /**
     * {@inheritdoc}
     */
    public function add(NodeConnectionInterface $connection)
    {
        $alias = $connection->getParameters()->alias;

        if ($alias === 'master') {
            $this->master = $connection;
        } else {
            $this->slaves[$alias ?: count($this->slaves)] = $connection;
        }

        $this->reset();
    }

    /**
     * {@inheritdoc}
     */
    public function remove(NodeConnectionInterface $connection)
    {
        if ($connection->getParameters()->alias === 'master') {
            $this->master = null;
            $this->reset();

            return true;
        } else {
            if (($id = array_search($connection, $this->slaves, true)) !== false) {
                unset($this->slaves[$id]);
                $this->reset();

                return true;
            }
        }

        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function getConnection(CommandInterface $command)
    {
        if ($this->current === null) {
            $this->check();
            $this->current = $this->strategy->isReadOperation($command)
                ? $this->pickSlave()
                : $this->master;

            return $this->current;
        }

        if ($this->current === $this->master) {
            return $this->current;
        }

        if (!$this->strategy->isReadOperation($command)) {
            $this->current = $this->master;
        }

        return $this->current;
    }

    /**
     * {@inheritdoc}
     */
    public function getConnectionById($connectionId)
    {
        if ($connectionId === 'master') {
            return $this->master;
        }

        if (isset($this->slaves[$connectionId])) {
            return $this->slaves[$connectionId];
        }

        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function switchTo($connection)
    {
        $this->check();

        if (!$connection instanceof NodeConnectionInterface) {
            $connection = $this->getConnectionById($connection);
        }
        if ($connection !== $this->master && !in_array($connection, $this->slaves, true)) {
            throw new InvalidArgumentException('Invalid connection or connection not found.');
        }

        $this->current = $connection;
    }

    /**
     * {@inheritdoc}
     */
    public function getCurrent()
    {
        return $this->current;
    }

    /**
     * {@inheritdoc}
     */
    public function getMaster()
    {
        return $this->master;
    }

    /**
     * {@inheritdoc}
     */
    public function getSlaves()
    {
        return array_values($this->slaves);
    }

    /**
     * Returns the underlying replication strategy.
     *
     * @return ReplicationStrategy
     */
    public function getReplicationStrategy()
    {
        return $this->strategy;
    }

    /**
     * Returns a random slave.
     *
     * @return NodeConnectionInterface
     */
    protected function pickSlave()
    {
        return $this->slaves[array_rand($this->slaves)];
    }

    /**
     * {@inheritdoc}
     */
    public function isConnected()
    {
        return $this->current ? $this->current->isConnected() : false;
    }

    /**
     * {@inheritdoc}
     */
    public function connect()
    {
        if ($this->current === null) {
            $this->check();
            $this->current = $this->pickSlave();
        }

        $this->current->connect();
    }

    /**
     * {@inheritdoc}
     */
    public function disconnect()
    {
        if ($this->master) {
            $this->master->disconnect();
        }

        foreach ($this->slaves as $connection) {
            $connection->disconnect();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function writeRequest(CommandInterface $command)
    {
        $this->getConnection($command)->writeRequest($command);
    }

    /**
     * {@inheritdoc}
     */
    public function readResponse(CommandInterface $command)
    {
        return $this->getConnection($command)->readResponse($command);
    }

    /**
     * {@inheritdoc}
     */
    public function executeCommand(CommandInterface $command)
    {
        return $this->getConnection($command)->executeCommand($command);
    }

    /**
     * {@inheritdoc}
     */
    public function __sleep()
    {
        return array('master', 'slaves', 'strategy');
    }
}

/* --------------------------------------------------------------------------- */

namespace Predis\Pipeline;

use SplQueue;
use Predis\ClientException;
use Predis\ClientInterface;
use Predis\Connection\ConnectionInterface;
use Predis\Connection\NodeConnectionInterface;
use Predis\Response\ErrorInterface as ErrorResponseInterface;
use Predis\Response\ResponseInterface;
use Predis\Response\ServerException;
use Predis\NotSupportedException;
use Predis\CommunicationException;
use Predis\Connection\Aggregate\ClusterInterface;
use Exception;
use InvalidArgumentException;
use Predis\ClientContextInterface;
use Predis\Command\CommandInterface;
use Predis\Connection\Aggregate\ReplicationInterface;

/**
 * Implementation of a command pipeline in which write and read operations of
 * Redis commands are pipelined to alleviate the effects of network round-trips.
 *
 * {@inheritdoc}
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class Pipeline implements ClientContextInterface
{
    private $client;
    private $pipeline;

    private $responses = array();
    private $running = false;

    /**
     * @param ClientInterface $client Client instance used by the context.
     */
    public function __construct(ClientInterface $client)
    {
        $this->client = $client;
        $this->pipeline = new SplQueue();
    }

    /**
     * Queues a command into the pipeline buffer.
     *
     * @param string $method    Command ID.
     * @param array  $arguments Arguments for the command.
     *
     * @return $this
     */
    public function __call($method, $arguments)
    {
        $command = $this->client->createCommand($method, $arguments);
        $this->recordCommand($command);

        return $this;
    }

    /**
     * Queues a command instance into the pipeline buffer.
     *
     * @param CommandInterface $command Command to be queued in the buffer.
     */
    protected function recordCommand(CommandInterface $command)
    {
        $this->pipeline->enqueue($command);
    }

    /**
     * Queues a command instance into the pipeline buffer.
     *
     * @param CommandInterface $command Command instance to be queued in the buffer.
     *
     * @return $this
     */
    public function executeCommand(CommandInterface $command)
    {
        $this->recordCommand($command);

        return $this;
    }

    /**
     * Throws an exception on -ERR responses returned by Redis.
     *
     * @param ConnectionInterface    $connection Redis connection that returned the error.
     * @param ErrorResponseInterface $response   Instance of the error response.
     *
     * @throws ServerException
     */
    protected function exception(ConnectionInterface $connection, ErrorResponseInterface $response)
    {
        $connection->disconnect();
        $message = $response->getMessage();

        throw new ServerException($message);
    }

    /**
     * Returns the underlying connection to be used by the pipeline.
     *
     * @return ConnectionInterface
     */
    protected function getConnection()
    {
        $connection = $this->getClient()->getConnection();

        if ($connection instanceof ReplicationInterface) {
            $connection->switchTo('master');
        }

        return $connection;
    }

    /**
     * Implements the logic to flush the queued commands and read the responses
     * from the current connection.
     *
     * @param ConnectionInterface $connection Current connection instance.
     * @param SplQueue            $commands   Queued commands.
     *
     * @return array
     */
    protected function executePipeline(ConnectionInterface $connection, SplQueue $commands)
    {
        foreach ($commands as $command) {
            $connection->writeRequest($command);
        }

        $responses = array();
        $exceptions = $this->throwServerExceptions();

        while (!$commands->isEmpty()) {
            $command = $commands->dequeue();
            $response = $connection->readResponse($command);

            if (!$response instanceof ResponseInterface) {
                $responses[] = $command->parseResponse($response);
            } elseif ($response instanceof ErrorResponseInterface && $exceptions) {
                $this->exception($connection, $response);
            } else {
                $responses[] = $response;
            }
        }

        return $responses;
    }

    /**
     * Flushes the buffer holding all of the commands queued so far.
     *
     * @param bool $send Specifies if the commands in the buffer should be sent to Redis.
     *
     * @return $this
     */
    public function flushPipeline($send = true)
    {
        if ($send && !$this->pipeline->isEmpty()) {
            $responses = $this->executePipeline($this->getConnection(), $this->pipeline);
            $this->responses = array_merge($this->responses, $responses);
        } else {
            $this->pipeline = new SplQueue();
        }

        return $this;
    }

    /**
     * Marks the running status of the pipeline.
     *
     * @param bool $bool Sets the running status of the pipeline.
     *
     * @throws ClientException
     */
    private function setRunning($bool)
    {
        if ($bool && $this->running) {
            throw new ClientException('The current pipeline context is already being executed.');
        }

        $this->running = $bool;
    }

    /**
     * Handles the actual execution of the whole pipeline.
     *
     * @param mixed $callable Optional callback for execution.
     *
     * @return array
     *
     * @throws Exception
     * @throws InvalidArgumentException
     */
    public function execute($callable = null)
    {
        if ($callable && !is_callable($callable)) {
            throw new InvalidArgumentException('The argument must be a callable object.');
        }

        $exception = null;
        $this->setRunning(true);

        try {
            if ($callable) {
                call_user_func($callable, $this);
            }

            $this->flushPipeline();
        } catch (Exception $exception) {
            // NOOP
        }

        $this->setRunning(false);

        if ($exception) {
            throw $exception;
        }

        return $this->responses;
    }

    /**
     * Returns if the pipeline should throw exceptions on server errors.
     *
     * @return bool
     */
    protected function throwServerExceptions()
    {
        return (bool) $this->client->getOptions()->exceptions;
    }

    /**
     * Returns the underlying client instance used by the pipeline object.
     *
     * @return ClientInterface
     */
    public function getClient()
    {
        return $this->client;
    }
}

/**
 * Command pipeline that writes commands to the servers but discards responses.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class FireAndForget extends Pipeline
{
    /**
     * {@inheritdoc}
     */
    protected function executePipeline(ConnectionInterface $connection, SplQueue $commands)
    {
        while (!$commands->isEmpty()) {
            $connection->writeRequest($commands->dequeue());
        }

        $connection->disconnect();

        return array();
    }
}

/**
 * Command pipeline that does not throw exceptions on connection errors, but
 * returns the exception instances as the rest of the response elements.
 *
 * @todo Awful naming!
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ConnectionErrorProof extends Pipeline
{
    /**
     * {@inheritdoc}
     */
    protected function getConnection()
    {
        return $this->getClient()->getConnection();
    }

    /**
     * {@inheritdoc}
     */
    protected function executePipeline(ConnectionInterface $connection, SplQueue $commands)
    {
        if ($connection instanceof NodeConnectionInterface) {
            return $this->executeSingleNode($connection, $commands);
        } elseif ($connection instanceof ClusterInterface) {
            return $this->executeCluster($connection, $commands);
        } else {
            $class = get_class($connection);

            throw new NotSupportedException("The connection class '$class' is not supported.");
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function executeSingleNode(NodeConnectionInterface $connection, SplQueue $commands)
    {
        $responses  = array();
        $sizeOfPipe = count($commands);

        foreach ($commands as $command) {
            try {
                $connection->writeRequest($command);
            } catch (CommunicationException $exception) {
                return array_fill(0, $sizeOfPipe, $exception);
            }
        }

        for ($i = 0; $i < $sizeOfPipe; $i++) {
            $command = $commands->dequeue();

            try {
                $responses[$i] = $connection->readResponse($command);
            } catch (CommunicationException $exception) {
                $add = count($commands) - count($responses);
                $responses = array_merge($responses, array_fill(0, $add, $exception));

                break;
            }
        }

        return $responses;
    }

    /**
     * {@inheritdoc}
     */
    protected function executeCluster(ClusterInterface $connection, SplQueue $commands)
    {
        $responses = array();
        $sizeOfPipe = count($commands);
        $exceptions = array();

        foreach ($commands as $command) {
            $cmdConnection = $connection->getConnection($command);

            if (isset($exceptions[spl_object_hash($cmdConnection)])) {
                continue;
            }

            try {
                $cmdConnection->writeRequest($command);
            } catch (CommunicationException $exception) {
                $exceptions[spl_object_hash($cmdConnection)] = $exception;
            }
        }

        for ($i = 0; $i < $sizeOfPipe; $i++) {
            $command = $commands->dequeue();

            $cmdConnection = $connection->getConnection($command);
            $connectionHash = spl_object_hash($cmdConnection);

            if (isset($exceptions[$connectionHash])) {
                $responses[$i] = $exceptions[$connectionHash];
                continue;
            }

            try {
                $responses[$i] = $cmdConnection->readResponse($command);
            } catch (CommunicationException $exception) {
                $responses[$i] = $exception;
                $exceptions[$connectionHash] = $exception;
            }
        }

        return $responses;
    }
}

/**
 * Command pipeline wrapped into a MULTI / EXEC transaction.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class Atomic extends Pipeline
{
    /**
     * {@inheritdoc}
     */
    public function __construct(ClientInterface $client)
    {
        if (!$client->getProfile()->supportsCommands(array('multi', 'exec', 'discard'))) {
            throw new ClientException(
                "The current profile does not support 'MULTI', 'EXEC' and 'DISCARD'."
            );
        }

        parent::__construct($client);
    }

    /**
     * {@inheritdoc}
     */
    protected function getConnection()
    {
        $connection = $this->getClient()->getConnection();

        if (!$connection instanceof NodeConnectionInterface) {
            $class = __CLASS__;

            throw new ClientException("The class '$class' does not support aggregate connections.");
        }

        return $connection;
    }

    /**
     * {@inheritdoc}
     */
    protected function executePipeline(ConnectionInterface $connection, SplQueue $commands)
    {
        $profile = $this->getClient()->getProfile();
        $connection->executeCommand($profile->createCommand('multi'));

        foreach ($commands as $command) {
            $connection->writeRequest($command);
        }

        foreach ($commands as $command) {
            $response = $connection->readResponse($command);

            if ($response instanceof ErrorResponseInterface) {
                $connection->executeCommand($profile->createCommand('discard'));
                throw new ServerException($response->getMessage());
            }
        }

        $executed = $connection->executeCommand($profile->createCommand('exec'));

        if (!isset($executed)) {
            // TODO: should be throwing a more appropriate exception.
            throw new ClientException(
                'The underlying transaction has been aborted by the server.'
            );
        }

        if (count($executed) !== count($commands)) {
            $expected = count($commands);
            $received = count($executed);

            throw new ClientException(
                "Invalid number of responses [expected $expected, received $received]."
            );
        }

        $responses = array();
        $sizeOfPipe = count($commands);
        $exceptions = $this->throwServerExceptions();

        for ($i = 0; $i < $sizeOfPipe; $i++) {
            $command  = $commands->dequeue();
            $response = $executed[$i];

            if (!$response instanceof ResponseInterface) {
                $responses[] = $command->parseResponse($response);
            } elseif ($response instanceof ErrorResponseInterface && $exceptions) {
                $this->exception($connection, $response);
            } else {
                $responses[] = $response;
            }

            unset($executed[$i]);
        }

        return $responses;
    }
}

/* --------------------------------------------------------------------------- */

namespace Predis\Cluster\Distributor;

use Predis\Cluster\Hash\HashGeneratorInterface;
use Exception;

/**
 * A distributor implements the logic to automatically distribute keys among
 * several nodes for client-side sharding.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface DistributorInterface
{
    /**
     * Adds a node to the distributor with an optional weight.
     *
     * @param mixed $node   Node object.
     * @param int   $weight Weight for the node.
     */
    public function add($node, $weight = null);

    /**
     * Removes a node from the distributor.
     *
     * @param mixed $node Node object.
     */
    public function remove($node);

    /**
     * Returns the corresponding slot of a node from the distributor using the
     * computed hash of a key.
     *
     * @param mixed $hash
     *
     * @return mixed
     */
    public function getSlot($hash);

    /**
     * Returns a node from the distributor using its assigned slot ID.
     *
     * @param mixed $slot
     *
     * @return mixed|null
     */
    public function getBySlot($slot);

    /**
     * Returns a node from the distributor using the computed hash of a key.
     *
     * @param mixed $hash
     *
     * @return mixed
     */
    public function getByHash($hash);

    /**
     * Returns a node from the distributor mapping to the specified value.
     *
     * @param string $value
     *
     * @return mixed
     */
    public function get($value);

    /**
     * Returns the underlying hash generator instance.
     *
     * @return HashGeneratorInterface
     */
    public function getHashGenerator();
}

/**
 * This class implements an hashring-based distributor that uses the same
 * algorithm of memcache to distribute keys in a cluster using client-side
 * sharding.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 * @author Lorenzo Castelli <lcastelli@gmail.com>
 */
class HashRing implements DistributorInterface, HashGeneratorInterface
{
    const DEFAULT_REPLICAS = 128;
    const DEFAULT_WEIGHT   = 100;

    private $ring;
    private $ringKeys;
    private $ringKeysCount;
    private $replicas;
    private $nodeHashCallback;
    private $nodes = array();

    /**
     * @param int   $replicas         Number of replicas in the ring.
     * @param mixed $nodeHashCallback Callback returning a string used to calculate the hash of nodes.
     */
    public function __construct($replicas = self::DEFAULT_REPLICAS, $nodeHashCallback = null)
    {
        $this->replicas = $replicas;
        $this->nodeHashCallback = $nodeHashCallback;
    }

    /**
     * Adds a node to the ring with an optional weight.
     *
     * @param mixed $node   Node object.
     * @param int   $weight Weight for the node.
     */
    public function add($node, $weight = null)
    {
        // In case of collisions in the hashes of the nodes, the node added
        // last wins, thus the order in which nodes are added is significant.
        $this->nodes[] = array(
            'object' => $node,
            'weight' => (int) $weight ?: $this::DEFAULT_WEIGHT
        );

        $this->reset();
    }

    /**
     * {@inheritdoc}
     */
    public function remove($node)
    {
        // A node is removed by resetting the ring so that it's recreated from
        // scratch, in order to reassign possible hashes with collisions to the
        // right node according to the order in which they were added in the
        // first place.
        for ($i = 0; $i < count($this->nodes); ++$i) {
            if ($this->nodes[$i]['object'] === $node) {
                array_splice($this->nodes, $i, 1);
                $this->reset();

                break;
            }
        }
    }

    /**
     * Resets the distributor.
     */
    private function reset()
    {
        unset(
            $this->ring,
            $this->ringKeys,
            $this->ringKeysCount
        );
    }

    /**
     * Returns the initialization status of the distributor.
     *
     * @return bool
     */
    private function isInitialized()
    {
        return isset($this->ringKeys);
    }

    /**
     * Calculates the total weight of all the nodes in the distributor.
     *
     * @return int
     */
    private function computeTotalWeight()
    {
        $totalWeight = 0;

        foreach ($this->nodes as $node) {
            $totalWeight += $node['weight'];
        }

        return $totalWeight;
    }

    /**
     * Initializes the distributor.
     */
    private function initialize()
    {
        if ($this->isInitialized()) {
            return;
        }

        if (!$this->nodes) {
            throw new EmptyRingException('Cannot initialize an empty hashring.');
        }

        $this->ring = array();
        $totalWeight = $this->computeTotalWeight();
        $nodesCount = count($this->nodes);

        foreach ($this->nodes as $node) {
            $weightRatio = $node['weight'] / $totalWeight;
            $this->addNodeToRing($this->ring, $node, $nodesCount, $this->replicas, $weightRatio);
        }

        ksort($this->ring, SORT_NUMERIC);
        $this->ringKeys = array_keys($this->ring);
        $this->ringKeysCount = count($this->ringKeys);
    }

    /**
     * Implements the logic needed to add a node to the hashring.
     *
     * @param array $ring        Source hashring.
     * @param mixed $node        Node object to be added.
     * @param int   $totalNodes  Total number of nodes.
     * @param int   $replicas    Number of replicas in the ring.
     * @param float $weightRatio Weight ratio for the node.
     */
    protected function addNodeToRing(&$ring, $node, $totalNodes, $replicas, $weightRatio)
    {
        $nodeObject = $node['object'];
        $nodeHash = $this->getNodeHash($nodeObject);
        $replicas = (int) round($weightRatio * $totalNodes * $replicas);

        for ($i = 0; $i < $replicas; $i++) {
            $key = crc32("$nodeHash:$i");
            $ring[$key] = $nodeObject;
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function getNodeHash($nodeObject)
    {
        if (!isset($this->nodeHashCallback)) {
            return (string) $nodeObject;
        }

        return call_user_func($this->nodeHashCallback, $nodeObject);
    }

    /**
     * {@inheritdoc}
     */
    public function hash($value)
    {
        return crc32($value);
    }

    /**
     * {@inheritdoc}
     */
    public function getByHash($hash)
    {
        return $this->ring[$this->getSlot($hash)];
    }

    /**
     * {@inheritdoc}
     */
    public function getBySlot($slot)
    {
        $this->initialize();

        if (isset($this->ring[$slot])) {
            return $this->ring[$slot];
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getSlot($hash)
    {
        $this->initialize();

        $ringKeys = $this->ringKeys;
        $upper = $this->ringKeysCount - 1;
        $lower = 0;

        while ($lower <= $upper) {
            $index = ($lower + $upper) >> 1;
            $item = $ringKeys[$index];

            if ($item > $hash) {
                $upper = $index - 1;
            } elseif ($item < $hash) {
                $lower = $index + 1;
            } else {
                return $item;
            }
        }

        return $ringKeys[$this->wrapAroundStrategy($upper, $lower, $this->ringKeysCount)];
    }

    /**
     * {@inheritdoc}
     */
    public function get($value)
    {
        $hash = $this->hash($value);
        $node = $this->getByHash($hash);

        return $node;
    }

    /**
     * Implements a strategy to deal with wrap-around errors during binary searches.
     *
     * @param int $upper
     * @param int $lower
     * @param int $ringKeysCount
     *
     * @return int
     */
    protected function wrapAroundStrategy($upper, $lower, $ringKeysCount)
    {
        // Binary search for the last item in ringkeys with a value less or
        // equal to the key. If no such item exists, return the last item.
        return $upper >= 0 ? $upper : $ringKeysCount - 1;
    }

    /**
     * {@inheritdoc}
     */
    public function getHashGenerator()
    {
        return $this;
    }
}

/**
 * This class implements an hashring-based distributor that uses the same
 * algorithm of libketama to distribute keys in a cluster using client-side
 * sharding.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 * @author Lorenzo Castelli <lcastelli@gmail.com>
 */
class KetamaRing extends HashRing
{
    const DEFAULT_REPLICAS = 160;

    /**
     * @param mixed $nodeHashCallback Callback returning a string used to calculate the hash of nodes.
     */
    public function __construct($nodeHashCallback = null)
    {
        parent::__construct($this::DEFAULT_REPLICAS, $nodeHashCallback);
    }

    /**
     * {@inheritdoc}
     */
    protected function addNodeToRing(&$ring, $node, $totalNodes, $replicas, $weightRatio)
    {
        $nodeObject = $node['object'];
        $nodeHash = $this->getNodeHash($nodeObject);
        $replicas = (int) floor($weightRatio * $totalNodes * ($replicas / 4));

        for ($i = 0; $i < $replicas; $i++) {
            $unpackedDigest = unpack('V4', md5("$nodeHash-$i", true));

            foreach ($unpackedDigest as $key) {
                $ring[$key] = $nodeObject;
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function hash($value)
    {
        $hash = unpack('V', md5($value, true));

        return $hash[1];
    }

    /**
     * {@inheritdoc}
     */
    protected function wrapAroundStrategy($upper, $lower, $ringKeysCount)
    {
        // Binary search for the first item in ringkeys with a value greater
        // or equal to the key. If no such item exists, return the first item.
        return $lower < $ringKeysCount ? $lower : 0;
    }
}

/**
 * Exception class that identifies empty rings.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class EmptyRingException extends Exception
{
}

/* --------------------------------------------------------------------------- */

namespace Predis\Response\Iterator;

use Predis\Connection\NodeConnectionInterface;
use Iterator;
use Countable;
use Predis\Response\ResponseInterface;
use OuterIterator;
use InvalidArgumentException;
use UnexpectedValueException;

/**
 * Iterator that abstracts the access to multibulk responses allowing them to be
 * consumed in a streamable fashion without keeping the whole payload in memory.
 *
 * This iterator does not support rewinding which means that the iteration, once
 * consumed, cannot be restarted.
 *
 * Always make sure that the whole iteration is consumed (or dropped) to prevent
 * protocol desynchronization issues.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
abstract class MultiBulkIterator implements Iterator, Countable, ResponseInterface
{
    protected $current;
    protected $position;
    protected $size;

    /**
     * {@inheritdoc}
     */
    public function rewind()
    {
        // NOOP
    }

    /**
     * {@inheritdoc}
     */
    public function current()
    {
        return $this->current;
    }

    /**
     * {@inheritdoc}
     */
    public function key()
    {
        return $this->position;
    }

    /**
     * {@inheritdoc}
     */
    public function next()
    {
        if (++$this->position < $this->size) {
            $this->current = $this->getValue();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function valid()
    {
        return $this->position < $this->size;
    }

    /**
     * Returns the number of items comprising the whole multibulk response.
     *
     * This method should be used instead of iterator_count() to get the size of
     * the current multibulk response since the former consumes the iteration to
     * count the number of elements, but our iterators do not support rewinding.
     *
     * @return int
     */
    public function count()
    {
        return $this->size;
    }

    /**
     * Returns the current position of the iterator.
     *
     * @return int
     */
    public function getPosition()
    {
        return $this->position;
    }

    /**
     * {@inheritdoc}
     */
    abstract protected function getValue();
}

/**
 * Streamable multibulk response.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class MultiBulk extends MultiBulkIterator
{
    private $connection;

    /**
     * @param NodeConnectionInterface $connection Connection to Redis.
     * @param int                     $size       Number of elements of the multibulk response.
     */
    public function __construct(NodeConnectionInterface $connection, $size)
    {
        $this->connection = $connection;
        $this->size = $size;
        $this->position = 0;
        $this->current  = $size > 0 ? $this->getValue() : null;
    }

    /**
     * Handles the synchronization of the client with the Redis protocol when
     * the garbage collector kicks in (e.g. when the iterator goes out of the
     * scope of a foreach or it is unset).
     */
    public function __destruct()
    {
        $this->drop(true);
    }

    /**
     * Drop queued elements that have not been read from the connection either
     * by consuming the rest of the multibulk response or quickly by closing the
     * underlying connection.
     *
     * @param bool $disconnect Consume the iterator or drop the connection.
     */
    public function drop($disconnect = false)
    {
        if ($disconnect) {
            if ($this->valid()) {
                $this->position = $this->size;
                $this->connection->disconnect();
            }
        } else {
            while ($this->valid()) {
                $this->next();
            }
        }
    }

    /**
     * Reads the next item of the multibulk response from the connection.
     *
     * @return mixed
     */
    protected function getValue()
    {
        return $this->connection->read();
    }
}

/**
 * Outer iterator consuming streamable multibulk responses by yielding tuples of
 * keys and values.
 *
 * This wrapper is useful for responses to commands such as `HGETALL` that can
 * be iterater as $key => $value pairs.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class MultiBulkTuple extends MultiBulk implements OuterIterator
{
    private $iterator;

    /**
     * @param MultiBulk $iterator Inner multibulk response iterator.
     */
    public function __construct(MultiBulk $iterator)
    {
        $this->checkPreconditions($iterator);

        $this->size = count($iterator) / 2;
        $this->iterator = $iterator;
        $this->position = $iterator->getPosition();
        $this->current  = $this->size > 0 ? $this->getValue() : null;
    }

    /**
     * Checks for valid preconditions.
     *
     * @param MultiBulk $iterator Inner multibulk response iterator.
     *
     * @throws \InvalidArgumentException
     * @throws \UnexpectedValueException
     */
    protected function checkPreconditions(MultiBulk $iterator)
    {
        if ($iterator->getPosition() !== 0) {
            throw new InvalidArgumentException(
                'Cannot initialize a tuple iterator using an already initiated iterator.'
            );
        }

        if (($size = count($iterator)) % 2 !== 0) {
            throw new UnexpectedValueException("Invalid response size for a tuple iterator.");
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getInnerIterator()
    {
        return $this->iterator;
    }

    /**
     * {@inheritdoc}
     */
    public function __destruct()
    {
        $this->iterator->drop(true);
    }

    /**
     * {@inheritdoc}
     */
    protected function getValue()
    {
        $k = $this->iterator->current();
        $this->iterator->next();

        $v = $this->iterator->current();
        $this->iterator->next();

        return array($k, $v);
    }
}

/* --------------------------------------------------------------------------- */

namespace Predis\Cluster\Hash;

/**
 * An hash generator implements the logic used to calculate the hash of a key to
 * distribute operations among Redis nodes.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface HashGeneratorInterface
{
    /**
     * Generates an hash from a string to be used for distribution.
     *
     * @param string $value String value.
     *
     * @return int
     */
    public function hash($value);
}

/**
 * Hash generator implementing the CRC-CCITT-16 algorithm used by redis-cluster.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class CRC16 implements HashGeneratorInterface
{
    private static $CCITT_16 = array(
        0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
        0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
        0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6,
        0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE,
        0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485,
        0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D,
        0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,
        0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC,
        0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823,
        0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B,
        0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12,
        0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A,
        0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41,
        0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,
        0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70,
        0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78,
        0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F,
        0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067,
        0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E,
        0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256,
        0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,
        0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
        0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C,
        0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634,
        0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB,
        0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3,
        0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A,
        0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92,
        0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9,
        0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1,
        0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8,
        0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0,
    );

    /**
     * {@inheritdoc}
     */
    public function hash($value)
    {
        // CRC-CCITT-16 algorithm
        $crc = 0;
        $CCITT_16 = self::$CCITT_16;
        $strlen = strlen($value);

        for ($i = 0; $i < $strlen; $i++) {
            $crc = (($crc << 8) ^ $CCITT_16[($crc >> 8) ^ ord($value[$i])]) & 0xFFFF;
        }

        return $crc;
    }
}

/* --------------------------------------------------------------------------- */

namespace Predis\Command\Processor;

use InvalidArgumentException;
use Predis\Command\CommandInterface;
use Predis\Command\PrefixableCommandInterface;
use ArrayAccess;
use ArrayIterator;

/**
 * A command processor processes Redis commands before they are sent to Redis.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
interface ProcessorInterface
{
    /**
     * Processes the given Redis command.
     *
     * @param CommandInterface $command Command instance.
     */
    public function process(CommandInterface $command);
}

/**
 * Default implementation of a command processors chain.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ProcessorChain implements ArrayAccess, ProcessorInterface
{
    private $processors = array();

    /**
     * @param array $processors List of instances of ProcessorInterface.
     */
    public function __construct($processors = array())
    {
        foreach ($processors as $processor) {
            $this->add($processor);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function add(ProcessorInterface $processor)
    {
        $this->processors[] = $processor;
    }

    /**
     * {@inheritdoc}
     */
    public function remove(ProcessorInterface $processor)
    {
        if (false !== $index = array_search($processor, $this->processors, true)) {
            unset($this[$index]);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function process(CommandInterface $command)
    {
        for ($i = 0; $i < $count = count($this->processors); $i++) {
            $this->processors[$i]->process($command);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getProcessors()
    {
        return $this->processors;
    }

    /**
     * Returns an iterator over the list of command processor in the chain.
     *
     * @return ArrayIterator
     */
    public function getIterator()
    {
        return new ArrayIterator($this->processors);
    }

    /**
     * Returns the number of command processors in the chain.
     *
     * @return int
     */
    public function count()
    {
        return count($this->processors);
    }

    /**
     * {@inheritdoc}
     */
    public function offsetExists($index)
    {
        return isset($this->processors[$index]);
    }

    /**
     * {@inheritdoc}
     */
    public function offsetGet($index)
    {
        return $this->processors[$index];
    }

    /**
     * {@inheritdoc}
     */
    public function offsetSet($index, $processor)
    {
        if (!$processor instanceof ProcessorInterface) {
            throw new InvalidArgumentException(
                "A processor chain accepts only instances of ".
                "'Predis\Command\Processor\ProcessorInterface'."
            );
        }

        $this->processors[$index] = $processor;
    }

    /**
     * {@inheritdoc}
     */
    public function offsetUnset($index)
    {
        unset($this->processors[$index]);
        $this->processors = array_values($this->processors);
    }
}

/**
 * Command processor capable of prefixing keys stored in the arguments of Redis
 * commands supported.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class KeyPrefixProcessor implements ProcessorInterface
{
    private $prefix;
    private $commands;

    /**
     * @param string $prefix Prefix for the keys.
     */
    public function __construct($prefix)
    {
        $this->prefix = $prefix;
        $this->commands = array(
            /* ---------------- Redis 1.2 ---------------- */
            'EXISTS'                    => 'self::first',
            'DEL'                       => 'self::all',
            'TYPE'                      => 'self::first',
            'KEYS'                      => 'self::first',
            'RENAME'                    => 'self::all',
            'RENAMENX'                  => 'self::all',
            'EXPIRE'                    => 'self::first',
            'EXPIREAT'                  => 'self::first',
            'TTL'                       => 'self::first',
            'MOVE'                      => 'self::first',
            'SORT'                      => 'self::sort',
            'DUMP'                      => 'self::first',
            'RESTORE'                   => 'self::first',
            'SET'                       => 'self::first',
            'SETNX'                     => 'self::first',
            'MSET'                      => 'self::interleaved',
            'MSETNX'                    => 'self::interleaved',
            'GET'                       => 'self::first',
            'MGET'                      => 'self::all',
            'GETSET'                    => 'self::first',
            'INCR'                      => 'self::first',
            'INCRBY'                    => 'self::first',
            'DECR'                      => 'self::first',
            'DECRBY'                    => 'self::first',
            'RPUSH'                     => 'self::first',
            'LPUSH'                     => 'self::first',
            'LLEN'                      => 'self::first',
            'LRANGE'                    => 'self::first',
            'LTRIM'                     => 'self::first',
            'LINDEX'                    => 'self::first',
            'LSET'                      => 'self::first',
            'LREM'                      => 'self::first',
            'LPOP'                      => 'self::first',
            'RPOP'                      => 'self::first',
            'RPOPLPUSH'                 => 'self::all',
            'SADD'                      => 'self::first',
            'SREM'                      => 'self::first',
            'SPOP'                      => 'self::first',
            'SMOVE'                     => 'self::skipLast',
            'SCARD'                     => 'self::first',
            'SISMEMBER'                 => 'self::first',
            'SINTER'                    => 'self::all',
            'SINTERSTORE'               => 'self::all',
            'SUNION'                    => 'self::all',
            'SUNIONSTORE'               => 'self::all',
            'SDIFF'                     => 'self::all',
            'SDIFFSTORE'                => 'self::all',
            'SMEMBERS'                  => 'self::first',
            'SRANDMEMBER'               => 'self::first',
            'ZADD'                      => 'self::first',
            'ZINCRBY'                   => 'self::first',
            'ZREM'                      => 'self::first',
            'ZRANGE'                    => 'self::first',
            'ZREVRANGE'                 => 'self::first',
            'ZRANGEBYSCORE'             => 'self::first',
            'ZCARD'                     => 'self::first',
            'ZSCORE'                    => 'self::first',
            'ZREMRANGEBYSCORE'          => 'self::first',
            /* ---------------- Redis 2.0 ---------------- */
            'SETEX'                     => 'self::first',
            'APPEND'                    => 'self::first',
            'SUBSTR'                    => 'self::first',
            'BLPOP'                     => 'self::skipLast',
            'BRPOP'                     => 'self::skipLast',
            'ZUNIONSTORE'               => 'self::zsetStore',
            'ZINTERSTORE'               => 'self::zsetStore',
            'ZCOUNT'                    => 'self::first',
            'ZRANK'                     => 'self::first',
            'ZREVRANK'                  => 'self::first',
            'ZREMRANGEBYRANK'           => 'self::first',
            'HSET'                      => 'self::first',
            'HSETNX'                    => 'self::first',
            'HMSET'                     => 'self::first',
            'HINCRBY'                   => 'self::first',
            'HGET'                      => 'self::first',
            'HMGET'                     => 'self::first',
            'HDEL'                      => 'self::first',
            'HEXISTS'                   => 'self::first',
            'HLEN'                      => 'self::first',
            'HKEYS'                     => 'self::first',
            'HVALS'                     => 'self::first',
            'HGETALL'                   => 'self::first',
            'SUBSCRIBE'                 => 'self::all',
            'UNSUBSCRIBE'               => 'self::all',
            'PSUBSCRIBE'                => 'self::all',
            'PUNSUBSCRIBE'              => 'self::all',
            'PUBLISH'                   => 'self::first',
            /* ---------------- Redis 2.2 ---------------- */
            'PERSIST'                   => 'self::first',
            'STRLEN'                    => 'self::first',
            'SETRANGE'                  => 'self::first',
            'GETRANGE'                  => 'self::first',
            'SETBIT'                    => 'self::first',
            'GETBIT'                    => 'self::first',
            'RPUSHX'                    => 'self::first',
            'LPUSHX'                    => 'self::first',
            'LINSERT'                   => 'self::first',
            'BRPOPLPUSH'                => 'self::skipLast',
            'ZREVRANGEBYSCORE'          => 'self::first',
            'WATCH'                     => 'self::all',
            /* ---------------- Redis 2.6 ---------------- */
            'PTTL'                      => 'self::first',
            'PEXPIRE'                   => 'self::first',
            'PEXPIREAT'                 => 'self::first',
            'PSETEX'                    => 'self::first',
            'INCRBYFLOAT'               => 'self::first',
            'BITOP'                     => 'self::skipFirst',
            'BITCOUNT'                  => 'self::first',
            'HINCRBYFLOAT'              => 'self::first',
            'EVAL'                      => 'self::evalKeys',
            'EVALSHA'                   => 'self::evalKeys',
            /* ---------------- Redis 2.8 ---------------- */
            'SSCAN'                     => 'self::first',
            'ZSCAN'                     => 'self::first',
            'HSCAN'                     => 'self::first',
            'PFADD'                     => 'self::first',
            'PFCOUNT'                   => 'self::all',
            'PFMERGE'                   => 'self::all',
            'ZLEXCOUNT'                 => 'self::first',
            'ZRANGEBYLEX'               => 'self::first',
            'ZREMRANGEBYLEX'            => 'self::first',
        );
    }

    /**
     * Sets a prefix that is applied to all the keys.
     *
     * @param string $prefix Prefix for the keys.
     */
    public function setPrefix($prefix)
    {
        $this->prefix = $prefix;
    }

    /**
     * Gets the current prefix.
     *
     * @return string
     */
    public function getPrefix()
    {
        return $this->prefix;
    }

    /**
     * {@inheritdoc}
     */
    public function process(CommandInterface $command)
    {
        if ($command instanceof PrefixableCommandInterface) {
            $command->prefixKeys($this->prefix);
        } elseif (isset($this->commands[$commandID = strtoupper($command->getId())])) {
            call_user_func($this->commands[$commandID], $command, $this->prefix);
        }
    }

    /**
     * Sets an handler for the specified command ID.
     *
     * The callback signature must have 2 parameters of the following types:
     *
     *   - Predis\Command\CommandInterface (command instance)
     *   - String (prefix)
     *
     * When the callback argument is omitted or NULL, the previously
     * associated handler for the specified command ID is removed.
     *
     * @param string $commandID The ID of the command to be handled.
     * @param mixed  $callback  A valid callable object or NULL.
     *
     * @throws \InvalidArgumentException
     */
    public function setCommandHandler($commandID, $callback = null)
    {
        $commandID = strtoupper($commandID);

        if (!isset($callback)) {
            unset($this->commands[$commandID]);

            return;
        }

        if (!is_callable($callback)) {
            throw new InvalidArgumentException(
                "Callback must be a valid callable object or NULL"
            );
        }

        $this->commands[$commandID] = $callback;
    }

    /**
     * {@inheritdoc}
     */
    public function __toString()
    {
        return $this->getPrefix();
    }

    /**
     * Applies the specified prefix only the first argument.
     *
     * @param CommandInterface $command Command instance.
     * @param string           $prefix  Prefix string.
     */
    public static function first(CommandInterface $command, $prefix)
    {
        if ($arguments = $command->getArguments()) {
            $arguments[0] = "$prefix{$arguments[0]}";
            $command->setRawArguments($arguments);
        }
    }

    /**
     * Applies the specified prefix to all the arguments.
     *
     * @param CommandInterface $command Command instance.
     * @param string           $prefix  Prefix string.
     */
    public static function all(CommandInterface $command, $prefix)
    {
        if ($arguments = $command->getArguments()) {
            foreach ($arguments as &$key) {
                $key = "$prefix$key";
            }

            $command->setRawArguments($arguments);
        }
    }

    /**
     * Applies the specified prefix only to even arguments in the list.
     *
     * @param CommandInterface $command Command instance.
     * @param string           $prefix  Prefix string.
     */
    public static function interleaved(CommandInterface $command, $prefix)
    {
        if ($arguments = $command->getArguments()) {
            $length = count($arguments);

            for ($i = 0; $i < $length; $i += 2) {
                $arguments[$i] = "$prefix{$arguments[$i]}";
            }

            $command->setRawArguments($arguments);
        }
    }

    /**
     * Applies the specified prefix to all the arguments but the first one.
     *
     * @param CommandInterface $command Command instance.
     * @param string           $prefix  Prefix string.
     */
    public static function skipFirst(CommandInterface $command, $prefix)
    {
        if ($arguments = $command->getArguments()) {
            $length = count($arguments);

            for ($i = 1; $i < $length; $i++) {
                $arguments[$i] = "$prefix{$arguments[$i]}";
            }

            $command->setRawArguments($arguments);
        }
    }

    /**
     * Applies the specified prefix to all the arguments but the last one.
     *
     * @param CommandInterface $command Command instance.
     * @param string           $prefix  Prefix string.
     */
    public static function skipLast(CommandInterface $command, $prefix)
    {
        if ($arguments = $command->getArguments()) {
            $length = count($arguments);

            for ($i = 0; $i < $length - 1; $i++) {
                $arguments[$i] = "$prefix{$arguments[$i]}";
            }

            $command->setRawArguments($arguments);
        }
    }

    /**
     * Applies the specified prefix to the keys of a SORT command.
     *
     * @param CommandInterface $command Command instance.
     * @param string           $prefix  Prefix string.
     */
    public static function sort(CommandInterface $command, $prefix)
    {
        if ($arguments = $command->getArguments()) {
            $arguments[0] = "$prefix{$arguments[0]}";

            if (($count = count($arguments)) > 1) {
                for ($i = 1; $i < $count; $i++) {
                    switch ($arguments[$i]) {
                        case 'BY':
                        case 'STORE':
                            $arguments[$i] = "$prefix{$arguments[++$i]}";
                            break;

                        case 'GET':
                            $value = $arguments[++$i];
                            if ($value !== '#') {
                                $arguments[$i] = "$prefix$value";
                            }
                            break;

                        case 'LIMIT';
                            $i += 2;
                            break;
                    }
                }
            }

            $command->setRawArguments($arguments);
        }
    }

    /**
     * Applies the specified prefix to the keys of an EVAL-based command.
     *
     * @param CommandInterface $command Command instance.
     * @param string           $prefix  Prefix string.
     */
    public static function evalKeys(CommandInterface $command, $prefix)
    {
        if ($arguments = $command->getArguments()) {
            for ($i = 2; $i < $arguments[1] + 2; $i++) {
                $arguments[$i] = "$prefix{$arguments[$i]}";
            }

            $command->setRawArguments($arguments);
        }
    }

    /**
     * Applies the specified prefix to the keys of Z[INTERSECTION|UNION]STORE.
     *
     * @param CommandInterface $command Command instance.
     * @param string           $prefix  Prefix string.
     */
    public static function zsetStore(CommandInterface $command, $prefix)
    {
        if ($arguments = $command->getArguments()) {
            $arguments[0] = "$prefix{$arguments[0]}";
            $length = ((int) $arguments[1]) + 2;

            for ($i = 2; $i < $length; $i++) {
                $arguments[$i] = "$prefix{$arguments[$i]}";
            }

            $command->setRawArguments($arguments);
        }
    }
}

/* --------------------------------------------------------------------------- */

namespace Predis\Protocol\Text;

use Predis\Command\CommandInterface;
use Predis\Connection\CompositeConnectionInterface;
use Predis\Protocol\ProtocolProcessorInterface;
use Predis\Protocol\RequestSerializerInterface;
use Predis\Protocol\ResponseReaderInterface;
use Predis\CommunicationException;
use Predis\Protocol\ProtocolException;
use Predis\Response\Status as StatusResponse;
use Predis\Response\Error as ErrorResponse;
use Predis\Response\Iterator\MultiBulk as MultiBulkIterator;

/**
 * Response reader for the standard Redis wire protocol.
 *
 * @link http://redis.io/topics/protocol
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ResponseReader implements ResponseReaderInterface
{
    protected $handlers;

    /**
     *
     */
    public function __construct()
    {
        $this->handlers = $this->getDefaultHandlers();
    }

    /**
     * Returns the default handlers for the supported type of responses.
     *
     * @return array
     */
    protected function getDefaultHandlers()
    {
        return array(
            '+' => new Handler\StatusResponse(),
            '-' => new Handler\ErrorResponse(),
            ':' => new Handler\IntegerResponse(),
            '$' => new Handler\BulkResponse(),
            '*' => new Handler\MultiBulkResponse(),
        );
    }

    /**
     * Sets the handler for the specified prefix identifying the response type.
     *
     * @param string                           $prefix  Identifier of the type of response.
     * @param Handler\ResponseHandlerInterface $handler Response handler.
     */
    public function setHandler($prefix, Handler\ResponseHandlerInterface $handler)
    {
        $this->handlers[$prefix] = $handler;
    }

    /**
     * Returns the response handler associated to a certain type of response.
     *
     * @param string $prefix Identifier of the type of response.
     *
     * @return Handler\ResponseHandlerInterface
     */
    public function getHandler($prefix)
    {
        if (isset($this->handlers[$prefix])) {
            return $this->handlers[$prefix];
        }

        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function read(CompositeConnectionInterface $connection)
    {
        $header = $connection->readLine();

        if ($header === '') {
            $this->onProtocolError($connection, 'Unexpected empty reponse header.');
        }

        $prefix = $header[0];

        if (!isset($this->handlers[$prefix])) {
            $this->onProtocolError($connection, "Unknown response prefix: '$prefix'.");
        }

        $payload = $this->handlers[$prefix]->handle($connection, substr($header, 1));

        return $payload;
    }

    /**
     * Handles protocol errors generated while reading responses from a
     * connection.
     *
     * @param CompositeConnectionInterface $connection Redis connection that generated the error.
     * @param string                       $message    Error message.
     */
    protected function onProtocolError(CompositeConnectionInterface $connection, $message)
    {
        CommunicationException::handle(
            new ProtocolException($connection, $message)
        );
    }
}

/**
 * Request serializer for the standard Redis wire protocol.
 *
 * @link http://redis.io/topics/protocol
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class RequestSerializer implements RequestSerializerInterface
{
    /**
     * {@inheritdoc}
     */
    public function serialize(CommandInterface $command)
    {
        $commandID = $command->getId();
        $arguments = $command->getArguments();

        $cmdlen = strlen($commandID);
        $reqlen = count($arguments) + 1;

        $buffer = "*{$reqlen}\r\n\${$cmdlen}\r\n{$commandID}\r\n";

        for ($i = 0, $reqlen--; $i < $reqlen; $i++) {
            $argument = $arguments[$i];
            $arglen = strlen($argument);
            $buffer .= "\${$arglen}\r\n{$argument}\r\n";
        }

        return $buffer;
    }
}

/**
 * Protocol processor for the standard Redis wire protocol.
 *
 * @link http://redis.io/topics/protocol
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ProtocolProcessor implements ProtocolProcessorInterface
{
    protected $mbiterable;
    protected $serializer;

    /**
     *
     */
    public function __construct()
    {
        $this->mbiterable = false;
        $this->serializer = new RequestSerializer();
    }

    /**
     * {@inheritdoc}
     */
    public function write(CompositeConnectionInterface $connection, CommandInterface $command)
    {
        $request = $this->serializer->serialize($command);
        $connection->writeBuffer($request);
    }

    /**
     * {@inheritdoc}
     */
    public function read(CompositeConnectionInterface $connection)
    {
        $chunk = $connection->readLine();
        $prefix = $chunk[0];
        $payload = substr($chunk, 1);

        switch ($prefix) {
            case '+':
                return new StatusResponse($payload);

            case '$':
                $size = (int) $payload;
                if ($size === -1) {
                    return null;
                }

                return substr($connection->readBuffer($size + 2), 0, -2);

            case '*':
                $count = (int) $payload;

                if ($count === -1) {
                    return null;
                }
                if ($this->mbiterable) {
                    return new MultiBulkIterator($connection, $count);
                }

                $multibulk = array();

                for ($i = 0; $i < $count; $i++) {
                    $multibulk[$i] = $this->read($connection);
                }

                return $multibulk;

            case ':':
                return (int) $payload;

            case '-':
                return new ErrorResponse($payload);

            default:
                CommunicationException::handle(new ProtocolException(
                    $connection, "Unknown response prefix: '$prefix'."
                ));

                return;
        }
    }

    /**
     * Enables or disables returning multibulk responses as specialized PHP
     * iterators used to stream bulk elements of a multibulk response instead
     * returning a plain array.
     *
     * Streamable multibulk responses are not globally supported by the
     * abstractions built-in into Predis, such as transactions or pipelines.
     * Use them with care!
     *
     * @param bool $value Enable or disable streamable multibulk responses.
     */
    public function useIterableMultibulk($value)
    {
        $this->mbiterable = (bool) $value;
    }
}

/**
 * Composite protocol processor for the standard Redis wire protocol using
 * pluggable handlers to serialize requests and deserialize responses.
 *
 * @link http://redis.io/topics/protocol
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class CompositeProtocolProcessor implements ProtocolProcessorInterface
{
    /*
     * @var RequestSerializerInterface
     */
    protected $serializer;

    /*
     * @var ResponseReaderInterface
     */
    protected $reader;

    /**
     * @param RequestSerializerInterface $serializer Request serializer.
     * @param ResponseReaderInterface    $reader     Response reader.
     */
    public function __construct(
        RequestSerializerInterface $serializer = null,
        ResponseReaderInterface $reader = null
    ) {
        $this->setRequestSerializer($serializer ?: new RequestSerializer());
        $this->setResponseReader($reader ?: new ResponseReader());
    }

    /**
     * {@inheritdoc}
     */
    public function write(CompositeConnectionInterface $connection, CommandInterface $command)
    {
        $connection->writeBuffer($this->serializer->serialize($command));
    }

    /**
     * {@inheritdoc}
     */
    public function read(CompositeConnectionInterface $connection)
    {
        return $this->reader->read($connection);
    }

    /**
     * Sets the request serializer used by the protocol processor.
     *
     * @param RequestSerializerInterface $serializer Request serializer.
     */
    public function setRequestSerializer(RequestSerializerInterface $serializer)
    {
        $this->serializer = $serializer;
    }

    /**
     * Returns the request serializer used by the protocol processor.
     *
     * @return RequestSerializerInterface
     */
    public function getRequestSerializer()
    {
        return $this->serializer;
    }

    /**
     * Sets the response reader used by the protocol processor.
     *
     * @param ResponseReaderInterface $reader Response reader.
     */
    public function setResponseReader(ResponseReaderInterface $reader)
    {
        $this->reader = $reader;
    }

    /**
     * Returns the Response reader used by the protocol processor.
     *
     * @return ResponseReaderInterface
     */
    public function getResponseReader()
    {
        return $this->reader;
    }
}

/* --------------------------------------------------------------------------- */

namespace Predis\PubSub;

use Iterator;
use Predis\ClientException;
use Predis\ClientInterface;
use Predis\Command\Command;
use Predis\NotSupportedException;
use Predis\Connection\AggregateConnectionInterface;
use InvalidArgumentException;

/**
 * Base implementation of a PUB/SUB consumer abstraction based on PHP iterators.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
abstract class AbstractConsumer implements Iterator
{
    const SUBSCRIBE    = 'subscribe';
    const UNSUBSCRIBE  = 'unsubscribe';
    const PSUBSCRIBE   = 'psubscribe';
    const PUNSUBSCRIBE = 'punsubscribe';
    const MESSAGE      = 'message';
    const PMESSAGE     = 'pmessage';
    const PONG         = 'pong';

    const STATUS_VALID       = 1;	// 0b0001
    const STATUS_SUBSCRIBED  = 2;	// 0b0010
    const STATUS_PSUBSCRIBED = 4;	// 0b0100

    private $position = null;
    private $statusFlags = self::STATUS_VALID;

    /**
     * Automatically stops the consumer when the garbage collector kicks in.
     */
    public function __destruct()
    {
        $this->stop(true);
    }

    /**
     * Checks if the specified flag is valid based on the state of the consumer.
     *
     * @param int $value Flag.
     *
     * @return bool
     */
    protected function isFlagSet($value)
    {
        return ($this->statusFlags & $value) === $value;
    }

    /**
     * Subscribes to the specified channels.
     *
     * @param mixed $channel,... One or more channel names.
     */
    public function subscribe($channel /*, ... */)
    {
        $this->writeRequest(self::SUBSCRIBE, func_get_args());
        $this->statusFlags |= self::STATUS_SUBSCRIBED;
    }

    /**
     * Unsubscribes from the specified channels.
     *
     * @param string ... One or more channel names.
     */
    public function unsubscribe(/* ... */)
    {
        $this->writeRequest(self::UNSUBSCRIBE, func_get_args());
    }

    /**
     * Subscribes to the specified channels using a pattern.
     *
     * @param mixed $pattern,... One or more channel name patterns.
     */
    public function psubscribe($pattern /* ... */)
    {
        $this->writeRequest(self::PSUBSCRIBE, func_get_args());
        $this->statusFlags |= self::STATUS_PSUBSCRIBED;
    }

    /**
     * Unsubscribes from the specified channels using a pattern.
     *
     * @param string ... One or more channel name patterns.
     */
    public function punsubscribe(/* ... */)
    {
        $this->writeRequest(self::PUNSUBSCRIBE, func_get_args());
    }

    /**
     * PING the server with an optional payload that will be echoed as a
     * PONG message in the pub/sub loop.
     *
     * @param string $payload Optional PING payload.
     */
    public function ping($payload = null)
    {
        $this->writeRequest('PING', array($payload));
    }

    /**
     * Closes the context by unsubscribing from all the subscribed channels. The
     * context can be forcefully closed by dropping the underlying connection.
     *
     * @param bool $drop Indicates if the context should be closed by dropping the connection.
     *
     * @return bool Returns false when there are no pending messages.
     */
    public function stop($drop = false)
    {
        if (!$this->valid()) {
            return false;
        }

        if ($drop) {
            $this->invalidate();
            $this->disconnect();
        } else {
            if ($this->isFlagSet(self::STATUS_SUBSCRIBED)) {
                $this->unsubscribe();
            }
            if ($this->isFlagSet(self::STATUS_PSUBSCRIBED)) {
                $this->punsubscribe();
            }
        }

        return !$drop;
    }

    /**
     * Closes the underlying connection when forcing a disconnection.
     */
    abstract protected function disconnect();

    /**
     * Writes a Redis command on the underlying connection.
     *
     * @param string $method    Command ID.
     * @param array  $arguments Arguments for the command.
     */
    abstract protected function writeRequest($method, $arguments);

    /**
     * {@inheritdoc}
     */
    public function rewind()
    {
        // NOOP
    }

    /**
     * Returns the last message payload retrieved from the server and generated
     * by one of the active subscriptions.
     *
     * @return array
     */
    public function current()
    {
        return $this->getValue();
    }

    /**
     * {@inheritdoc}
     */
    public function key()
    {
        return $this->position;
    }

    /**
     * {@inheritdoc}
     */
    public function next()
    {
        if ($this->valid()) {
            $this->position++;
        }

        return $this->position;
    }

    /**
     * Checks if the the consumer is still in a valid state to continue.
     *
     * @return bool
     */
    public function valid()
    {
        $isValid = $this->isFlagSet(self::STATUS_VALID);
        $subscriptionFlags = self::STATUS_SUBSCRIBED | self::STATUS_PSUBSCRIBED;
        $hasSubscriptions = ($this->statusFlags & $subscriptionFlags) > 0;

        return $isValid && $hasSubscriptions;
    }

    /**
     * Resets the state of the consumer.
     */
    protected function invalidate()
    {
        $this->statusFlags = 0;	// 0b0000;
    }

    /**
     * Waits for a new message from the server generated by one of the active
     * subscriptions and returns it when available.
     *
     * @return array
     */
    abstract protected function getValue();
}

/**
 * Method-dispatcher loop built around the client-side abstraction of a Redis
 * PUB / SUB context.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class DispatcherLoop
{
    private $pubsub;

    protected $callbacks;
    protected $defaultCallback;
    protected $subscriptionCallback;

    /**
     * @param Consumer $pubsub PubSub consumer instance used by the loop.
     */
    public function __construct(Consumer $pubsub)
    {
        $this->callbacks = array();
        $this->pubsub = $pubsub;
    }

    /**
     * Checks if the passed argument is a valid callback.
     *
     * @param mixed $callable A callback.
     *
     * @throws \InvalidArgumentException
     */
    protected function assertCallback($callable)
    {
        if (!is_callable($callable)) {
            throw new InvalidArgumentException('The given argument must be a callable object.');
        }
    }

    /**
     * Returns the underlying PUB / SUB context.
     *
     * @return Consumer
     */
    public function getPubSubConsumer()
    {
        return $this->pubsub;
    }

    /**
     * Sets a callback that gets invoked upon new subscriptions.
     *
     * @param mixed $callable A callback.
     */
    public function subscriptionCallback($callable = null)
    {
        if (isset($callable)) {
            $this->assertCallback($callable);
        }

        $this->subscriptionCallback = $callable;
    }

    /**
     * Sets a callback that gets invoked when a message is received on a
     * channel that does not have an associated callback.
     *
     * @param mixed $callable A callback.
     */
    public function defaultCallback($callable = null)
    {
        if (isset($callable)) {
            $this->assertCallback($callable);
        }

        $this->subscriptionCallback = $callable;
    }

    /**
     * Binds a callback to a channel.
     *
     * @param string   $channel  Channel name.
     * @param Callable $callback A callback.
     */
    public function attachCallback($channel, $callback)
    {
        $callbackName = $this->getPrefixKeys() . $channel;

        $this->assertCallback($callback);
        $this->callbacks[$callbackName] = $callback;
        $this->pubsub->subscribe($channel);
    }

    /**
     * Stops listening to a channel and removes the associated callback.
     *
     * @param string $channel Redis channel.
     */
    public function detachCallback($channel)
    {
        $callbackName = $this->getPrefixKeys() . $channel;

        if (isset($this->callbacks[$callbackName])) {
            unset($this->callbacks[$callbackName]);
            $this->pubsub->unsubscribe($channel);
        }
    }

    /**
     * Starts the dispatcher loop.
     */
    public function run()
    {
        foreach ($this->pubsub as $message) {
            $kind = $message->kind;

            if ($kind !== Consumer::MESSAGE && $kind !== Consumer::PMESSAGE) {
                if (isset($this->subscriptionCallback)) {
                    $callback = $this->subscriptionCallback;
                    call_user_func($callback, $message);
                }

                continue;
            }

            if (isset($this->callbacks[$message->channel])) {
                $callback = $this->callbacks[$message->channel];
                call_user_func($callback, $message->payload);
            } elseif (isset($this->defaultCallback)) {
                $callback = $this->defaultCallback;
                call_user_func($callback, $message);
            }
        }
    }

    /**
     * Terminates the dispatcher loop.
     */
    public function stop()
    {
        $this->pubsub->stop();
    }

    /**
     * Return the prefix used for keys
     *
     * @return string
     */
    protected function getPrefixKeys()
    {
        $options = $this->pubsub->getClient()->getOptions();

        if (isset($options->prefix)) {
            return $options->prefix->getPrefix();
        }

        return '';
    }
}

/**
 * PUB/SUB consumer abstraction.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class Consumer extends AbstractConsumer
{
    private $client;
    private $options;

    /**
     * @param ClientInterface $client  Client instance used by the consumer.
     * @param array           $options Options for the consumer initialization.
     */
    public function __construct(ClientInterface $client, array $options = null)
    {
        $this->checkCapabilities($client);

        $this->options = $options ?: array();
        $this->client = $client;

        $this->genericSubscribeInit('subscribe');
        $this->genericSubscribeInit('psubscribe');
    }

    /**
     * Returns the underlying client instance used by the pub/sub iterator.
     *
     * @return ClientInterface
     */
    public function getClient()
    {
        return $this->client;
    }

    /**
     * Checks if the client instance satisfies the required conditions needed to
     * initialize a PUB/SUB consumer.
     *
     * @param ClientInterface $client Client instance used by the consumer.
     *
     * @throws NotSupportedException
     */
    private function checkCapabilities(ClientInterface $client)
    {
        if ($client->getConnection() instanceof AggregateConnectionInterface) {
            throw new NotSupportedException(
                'Cannot initialize a PUB/SUB consumer over aggregate connections.'
            );
        }

        $commands = array('publish', 'subscribe', 'unsubscribe', 'psubscribe', 'punsubscribe');

        if ($client->getProfile()->supportsCommands($commands) === false) {
            throw new NotSupportedException(
                'The current profile does not support PUB/SUB related commands.'
            );
        }
    }

    /**
     * This method shares the logic to handle both SUBSCRIBE and PSUBSCRIBE.
     *
     * @param string $subscribeAction Type of subscription.
     */
    private function genericSubscribeInit($subscribeAction)
    {
        if (isset($this->options[$subscribeAction])) {
            $this->$subscribeAction($this->options[$subscribeAction]);
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function writeRequest($method, $arguments)
    {
        $this->client->getConnection()->writeRequest(
            $this->client->createCommand($method,
                Command::normalizeArguments($arguments)
            )
        );
    }

    /**
     * {@inheritdoc}
     */
    protected function disconnect()
    {
        $this->client->disconnect();
    }

    /**
     * {@inheritdoc}
     */
    protected function getValue()
    {
        $response = $this->client->getConnection()->read();

        switch ($response[0]) {
            case self::SUBSCRIBE:
            case self::UNSUBSCRIBE:
            case self::PSUBSCRIBE:
            case self::PUNSUBSCRIBE:
                if ($response[2] === 0) {
                    $this->invalidate();
                }
                // The missing break here is intentional as we must process
                // subscriptions and unsubscriptions as standard messages.
                // no break

            case self::MESSAGE:
                return (object) array(
                    'kind'    => $response[0],
                    'channel' => $response[1],
                    'payload' => $response[2],
                );

            case self::PMESSAGE:
                return (object) array(
                    'kind'    => $response[0],
                    'pattern' => $response[1],
                    'channel' => $response[2],
                    'payload' => $response[3],
                );

            case self::PONG:
                return (object) array(
                    'kind'    => $response[0],
                    'payload' => $response[1],
                );

            default:
                throw new ClientException(
                    "Unknown message type '{$response[0]}' received in the PUB/SUB context."
                );
        }
    }
}

/* --------------------------------------------------------------------------- */

namespace Predis\Transaction;

use Predis\PredisException;
use Exception;
use InvalidArgumentException;
use SplQueue;
use Predis\ClientContextInterface;
use Predis\ClientException;
use Predis\ClientInterface;
use Predis\CommunicationException;
use Predis\NotSupportedException;
use Predis\Response\ErrorInterface as ErrorResponseInterface;
use Predis\Response\ServerException;
use Predis\Response\Status as StatusResponse;
use Predis\Command\CommandInterface;
use Predis\Connection\AggregateConnectionInterface;
use Predis\Protocol\ProtocolException;

/**
 * Utility class used to track the state of a MULTI / EXEC transaction.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class MultiExecState
{
    const INITIALIZED = 1;    // 0b00001
    const INSIDEBLOCK = 2;    // 0b00010
    const DISCARDED   = 4;    // 0b00100
    const CAS         = 8;    // 0b01000
    const WATCH       = 16;   // 0b10000

    private $flags;

    /**
     *
     */
    public function __construct()
    {
        $this->flags = 0;
    }

    /**
     * Sets the internal state flags.
     *
     * @param int $flags Set of flags
     */
    public function set($flags)
    {
        $this->flags = $flags;
    }

    /**
     * Gets the internal state flags.
     *
     * @return int
     */
    public function get()
    {
        return $this->flags;
    }

    /**
     * Sets one or more flags.
     *
     * @param int $flags Set of flags
     */
    public function flag($flags)
    {
        $this->flags |= $flags;
    }

    /**
     * Resets one or more flags.
     *
     * @param int $flags Set of flags
     */
    public function unflag($flags)
    {
        $this->flags &= ~$flags;
    }

    /**
     * Returns if the specified flag or set of flags is set.
     *
     * @param int $flags Flag
     *
     * @return bool
     */
    public function check($flags)
    {
        return ($this->flags & $flags) === $flags;
    }

    /**
     * Resets the state of a transaction.
     */
    public function reset()
    {
        $this->flags = 0;
    }

    /**
     * Returns the state of the RESET flag.
     *
     * @return bool
     */
    public function isReset()
    {
        return $this->flags === 0;
    }

    /**
     * Returns the state of the INITIALIZED flag.
     *
     * @return bool
     */
    public function isInitialized()
    {
        return $this->check(self::INITIALIZED);
    }

    /**
     * Returns the state of the INSIDEBLOCK flag.
     *
     * @return bool
     */
    public function isExecuting()
    {
        return $this->check(self::INSIDEBLOCK);
    }

    /**
     * Returns the state of the CAS flag.
     *
     * @return bool
     */
    public function isCAS()
    {
        return $this->check(self::CAS);
    }

    /**
     * Returns if WATCH is allowed in the current state.
     *
     * @return bool
     */
    public function isWatchAllowed()
    {
        return $this->check(self::INITIALIZED) && !$this->check(self::CAS);
    }

    /**
     * Returns the state of the WATCH flag.
     *
     * @return bool
     */
    public function isWatching()
    {
        return $this->check(self::WATCH);
    }

    /**
     * Returns the state of the DISCARDED flag.
     *
     * @return bool
     */
    public function isDiscarded()
    {
        return $this->check(self::DISCARDED);
    }
}

/**
 * Client-side abstraction of a Redis transaction based on MULTI / EXEC.
 *
 * {@inheritdoc}
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class MultiExec implements ClientContextInterface
{
    private $state;

    protected $client;
    protected $commands;
    protected $exceptions = true;
    protected $attempts   = 0;
    protected $watchKeys  = array();
    protected $modeCAS    = false;

    /**
     * @param ClientInterface $client  Client instance used by the transaction.
     * @param array           $options Initialization options.
     */
    public function __construct(ClientInterface $client, array $options = null)
    {
        $this->assertClient($client);

        $this->client = $client;
        $this->state = new MultiExecState();

        $this->configure($client, $options ?: array());
        $this->reset();
    }

    /**
     * Checks if the passed client instance satisfies the required conditions
     * needed to initialize the transaction object.
     *
     * @param ClientInterface $client Client instance used by the transaction object.
     *
     * @throws NotSupportedException
     */
    private function assertClient(ClientInterface $client)
    {
        if ($client->getConnection() instanceof AggregateConnectionInterface) {
            throw new NotSupportedException(
                'Cannot initialize a MULTI/EXEC transaction over aggregate connections.'
            );
        }

        if (!$client->getProfile()->supportsCommands(array('MULTI', 'EXEC', 'DISCARD'))) {
            throw new NotSupportedException(
                'The current profile does not support MULTI, EXEC and DISCARD.'
            );
        }
    }

    /**
     * Configures the transaction using the provided options.
     *
     * @param ClientInterface $client  Underlying client instance.
     * @param array           $options Array of options for the transaction.
     **/
    protected function configure(ClientInterface $client, array $options)
    {
        if (isset($options['exceptions'])) {
            $this->exceptions = (bool) $options['exceptions'];
        } else {
            $this->exceptions = $client->getOptions()->exceptions;
        }

        if (isset($options['cas'])) {
            $this->modeCAS = (bool) $options['cas'];
        }

        if (isset($options['watch']) && $keys = $options['watch']) {
            $this->watchKeys = $keys;
        }

        if (isset($options['retry'])) {
            $this->attempts = (int) $options['retry'];
        }
    }

    /**
     * Resets the state of the transaction.
     */
    protected function reset()
    {
        $this->state->reset();
        $this->commands = new SplQueue();
    }

    /**
     * Initializes the transaction context.
     */
    protected function initialize()
    {
        if ($this->state->isInitialized()) {
            return;
        }

        if ($this->modeCAS) {
            $this->state->flag(MultiExecState::CAS);
        }

        if ($this->watchKeys) {
            $this->watch($this->watchKeys);
        }

        $cas = $this->state->isCAS();
        $discarded = $this->state->isDiscarded();

        if (!$cas || ($cas && $discarded)) {
            $this->call('MULTI');

            if ($discarded) {
                $this->state->unflag(MultiExecState::CAS);
            }
        }

        $this->state->unflag(MultiExecState::DISCARDED);
        $this->state->flag(MultiExecState::INITIALIZED);
    }

    /**
     * Dynamically invokes a Redis command with the specified arguments.
     *
     * @param string $method    Command ID.
     * @param array  $arguments Arguments for the command.
     *
     * @return mixed
     */
    public function __call($method, $arguments)
    {
        return $this->executeCommand(
            $this->client->createCommand($method, $arguments)
        );
    }

    /**
     * Executes a Redis command bypassing the transaction logic.
     *
     * @param string $commandID Command ID.
     * @param array  $arguments Arguments for the command.
     *
     * @return mixed
     *
     * @throws ServerException
     */
    protected function call($commandID, array $arguments = array())
    {
        $response = $this->client->executeCommand(
            $this->client->createCommand($commandID, $arguments)
        );

        if ($response instanceof ErrorResponseInterface) {
            throw new ServerException($response->getMessage());
        }

        return $response;
    }

    /**
     * Executes the specified Redis command.
     *
     * @param CommandInterface $command Command instance.
     *
     * @return $this|mixed
     *
     * @throws AbortedMultiExecException
     * @throws CommunicationException
     */
    public function executeCommand(CommandInterface $command)
    {
        $this->initialize();
        if ($this->state->isCAS()) {
            return $this->client->executeCommand($command);
        }

        $response = $this->client->getConnection()->executeCommand($command);

        if ($response instanceof StatusResponse && $response == 'QUEUED') {
            $this->commands->enqueue($command);
        } elseif ($response instanceof ErrorResponseInterface) {
            throw new AbortedMultiExecException($this, $response->getMessage());
        } else {
            $this->onProtocolError('The server did not return a +QUEUED status response.');
        }

        return $this;
    }

    /**
     * Executes WATCH against one or more keys.
     *
     * @param string|array $keys One or more keys.
     *
     * @return mixed
     *
     * @throws NotSupportedException
     * @throws ClientException
     */
    public function watch($keys)
    {
        if (!$this->client->getProfile()->supportsCommand('WATCH')) {
            throw new NotSupportedException('WATCH is not supported by the current profile.');
        }

        if ($this->state->isWatchAllowed()) {
            throw new ClientException('Sending WATCH after MULTI is not allowed.');
        }

        $response = $this->call('WATCH', is_array($keys) ? $keys : array($keys));
        $this->state->flag(MultiExecState::WATCH);

        return $response;
    }

    /**
     * Finalizes the transaction by executing MULTI on the server.
     *
     * @return MultiExec
     */
    public function multi()
    {
        if ($this->state->check(MultiExecState::INITIALIZED | MultiExecState::CAS)) {
            $this->state->unflag(MultiExecState::CAS);
            $this->call('MULTI');
        } else {
            $this->initialize();
        }

        return $this;
    }

    /**
     * Executes UNWATCH.
     *
     * @return MultiExec
     *
     * @throws NotSupportedException
     */
    public function unwatch()
    {
        if (!$this->client->getProfile()->supportsCommand('UNWATCH')) {
            throw new NotSupportedException(
                'UNWATCH is not supported by the current profile.'
            );
        }

        $this->state->unflag(MultiExecState::WATCH);
        $this->__call('UNWATCH', array());

        return $this;
    }

    /**
     * Resets the transaction by UNWATCH-ing the keys that are being WATCHed and
     * DISCARD-ing pending commands that have been already sent to the server.
     *
     * @return MultiExec
     */
    public function discard()
    {
        if ($this->state->isInitialized()) {
            $this->call($this->state->isCAS() ? 'UNWATCH' : 'DISCARD');

            $this->reset();
            $this->state->flag(MultiExecState::DISCARDED);
        }

        return $this;
    }

    /**
     * Executes the whole transaction.
     *
     * @return mixed
     */
    public function exec()
    {
        return $this->execute();
    }

    /**
     * Checks the state of the transaction before execution.
     *
     * @param mixed $callable Callback for execution.
     *
     * @throws InvalidArgumentException
     * @throws ClientException
     */
    private function checkBeforeExecution($callable)
    {
        if ($this->state->isExecuting()) {
            throw new ClientException(
                'Cannot invoke "execute" or "exec" inside an active transaction context.'
            );
        }

        if ($callable) {
            if (!is_callable($callable)) {
                throw new InvalidArgumentException('The argument must be a callable object.');
            }

            if (!$this->commands->isEmpty()) {
                $this->discard();

                throw new ClientException(
                    'Cannot execute a transaction block after using fluent interface.'
                );
            }
        } elseif ($this->attempts) {
            $this->discard();

            throw new ClientException(
                'Automatic retries are supported only when a callable block is provided.'
            );
        }
    }

    /**
     * Handles the actual execution of the whole transaction.
     *
     * @param mixed $callable Optional callback for execution.
     *
     * @return array
     *
     * @throws CommunicationException
     * @throws AbortedMultiExecException
     * @throws ServerException
     */
    public function execute($callable = null)
    {
        $this->checkBeforeExecution($callable);

        $execResponse = null;
        $attempts = $this->attempts;

        do {
            if ($callable) {
                $this->executeTransactionBlock($callable);
            }

            if ($this->commands->isEmpty()) {
                if ($this->state->isWatching()) {
                    $this->discard();
                }

                return null;
            }

            $execResponse = $this->call('EXEC');

            if ($execResponse === null) {
                if ($attempts === 0) {
                    throw new AbortedMultiExecException(
                        $this, 'The current transaction has been aborted by the server.'
                    );
                }

                $this->reset();

                continue;
            }

            break;
        } while ($attempts-- > 0);

        $response = array();
        $commands = $this->commands;
        $size = count($execResponse);

        if ($size !== count($commands)) {
            $this->onProtocolError('EXEC returned an unexpected number of response items.');
        }

        for ($i = 0; $i < $size; $i++) {
            $cmdResponse = $execResponse[$i];

            if ($cmdResponse instanceof ErrorResponseInterface && $this->exceptions) {
                throw new ServerException($cmdResponse->getMessage());
            }

            $response[$i] = $commands->dequeue()->parseResponse($cmdResponse);
        }

        return $response;
    }

    /**
     * Passes the current transaction object to a callable block for execution.
     *
     * @param mixed $callable Callback.
     *
     * @throws CommunicationException
     * @throws ServerException
     */
    protected function executeTransactionBlock($callable)
    {
        $exception = null;
        $this->state->flag(MultiExecState::INSIDEBLOCK);

        try {
            call_user_func($callable, $this);
        } catch (CommunicationException $exception) {
            // NOOP
        } catch (ServerException $exception) {
            // NOOP
        } catch (Exception $exception) {
            $this->discard();
        }

        $this->state->unflag(MultiExecState::INSIDEBLOCK);

        if ($exception) {
            throw $exception;
        }
    }

    /**
     * Helper method for protocol errors encountered inside the transaction.
     *
     * @param string $message Error message.
     */
    private function onProtocolError($message)
    {
        // Since a MULTI/EXEC block cannot be initialized when using aggregate
        // connections we can safely assume that Predis\Client::getConnection()
        // will return a Predis\Connection\NodeConnectionInterface instance.
        CommunicationException::handle(new ProtocolException(
            $this->client->getConnection(), $message
        ));
    }
}

/**
 * Exception class that identifies a MULTI / EXEC transaction aborted by Redis.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class AbortedMultiExecException extends PredisException
{
    private $transaction;

    /**
     * @param MultiExec $transaction Transaction that generated the exception.
     * @param string    $message     Error message.
     * @param int       $code        Error code.
     */
    public function __construct(MultiExec $transaction, $message, $code = null)
    {
        parent::__construct($message, $code);
        $this->transaction = $transaction;
    }

    /**
     * Returns the transaction that generated the exception.
     *
     * @return MultiExec
     */
    public function getTransaction()
    {
        return $this->transaction;
    }
}

/* --------------------------------------------------------------------------- */

namespace Predis\Session;

use SessionHandlerInterface;
use Predis\ClientInterface;

/**
 * Session handler class that relies on Predis\Client to store PHP's sessions
 * data into one or multiple Redis servers.
 *
 * This class is mostly intended for PHP 5.4 but it can be used under PHP 5.3
 * provided that a polyfill for `SessionHandlerInterface` is defined by either
 * you or an external package such as `symfony/http-foundation`.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class Handler implements SessionHandlerInterface
{
    protected $client;
    protected $ttl;

    /**
     * @param ClientInterface $client  Fully initialized client instance.
     * @param array           $options Session handler options.
     */
    public function __construct(ClientInterface $client, array $options = array())
    {
        $this->client = $client;

        if (isset($options['gc_maxlifetime'])) {
            $this->ttl = (int) $options['gc_maxlifetime'];
        } else {
            $this->ttl = ini_get('session.gc_maxlifetime');
        }
    }

    /**
     * Registers this instance as the current session handler.
     */
    public function register()
    {
        if (version_compare(PHP_VERSION, '5.4.0') >= 0) {
            session_set_save_handler($this, true);
        } else {
            session_set_save_handler(
                array($this, 'open'),
                array($this, 'close'),
                array($this, 'read'),
                array($this, 'write'),
                array($this, 'destroy'),
                array($this, 'gc')
            );
        }
    }

    /**
     * {@inheritdoc}
     */
    public function open($save_path, $session_id)
    {
        // NOOP
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function close()
    {
        // NOOP
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function gc($maxlifetime)
    {
        // NOOP
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function read($session_id)
    {
        if ($data = $this->client->get($session_id)) {
            return $data;
        }

        return '';
    }
    /**
     * {@inheritdoc}
     */
    public function write($session_id, $session_data)
    {
        $this->client->setex($session_id, $this->ttl, $session_data);

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function destroy($session_id)
    {
        $this->client->del($session_id);

        return true;
    }

    /**
     * Returns the underlying client instance.
     *
     * @return ClientInterface
     */
    public function getClient()
    {
        return $this->client;
    }

    /**
     * Returns the session max lifetime value.
     *
     * @return int
     */
    public function getMaxLifeTime()
    {
        return $this->ttl;
    }
}

/* --------------------------------------------------------------------------- */

namespace Predis\Monitor;

use Iterator;
use Predis\ClientInterface;
use Predis\NotSupportedException;
use Predis\Connection\AggregateConnectionInterface;

/**
 * Redis MONITOR consumer.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class Consumer implements Iterator
{
    private $client;
    private $valid;
    private $position;

    /**
     * @param ClientInterface $client Client instance used by the consumer.
     */
    public function __construct(ClientInterface $client)
    {
        $this->assertClient($client);

        $this->client = $client;

        $this->start();
    }

    /**
     * Automatically stops the consumer when the garbage collector kicks in.
     */
    public function __destruct()
    {
        $this->stop();
    }

    /**
     * Checks if the passed client instance satisfies the required conditions
     * needed to initialize a monitor consumer.
     *
     * @param ClientInterface $client Client instance used by the consumer.
     *
     * @throws NotSupportedException
     */
    private function assertClient(ClientInterface $client)
    {
        if ($client->getConnection() instanceof AggregateConnectionInterface) {
            throw new NotSupportedException(
                'Cannot initialize a monitor consumer over aggregate connections.'
            );
        }

        if ($client->getProfile()->supportsCommand('MONITOR') === false) {
            throw new NotSupportedException("The current profile does not support 'MONITOR'.");
        }
    }

    /**
     * Initializes the consumer and sends the MONITOR command to the server.
     */
    protected function start()
    {
        $this->client->executeCommand(
            $this->client->createCommand('MONITOR')
        );
        $this->valid = true;
    }

    /**
     * Stops the consumer. Internally this is done by disconnecting from server
     * since there is no way to terminate the stream initialized by MONITOR.
     */
    public function stop()
    {
        $this->client->disconnect();
        $this->valid = false;
    }

    /**
     * {@inheritdoc}
     */
    public function rewind()
    {
        // NOOP
    }

    /**
     * Returns the last message payload retrieved from the server.
     *
     * @return Object
     */
    public function current()
    {
        return $this->getValue();
    }

    /**
     * {@inheritdoc}
     */
    public function key()
    {
        return $this->position;
    }

    /**
     * {@inheritdoc}
     */
    public function next()
    {
        $this->position++;
    }

    /**
     * Checks if the the consumer is still in a valid state to continue.
     *
     * @return bool
     */
    public function valid()
    {
        return $this->valid;
    }

    /**
     * Waits for a new message from the server generated by MONITOR and returns
     * it when available.
     *
     * @return Object
     */
    private function getValue()
    {
        $database = 0;
        $client = null;
        $event = $this->client->getConnection()->read();

        $callback = function ($matches) use (&$database, &$client) {
            if (2 === $count = count($matches)) {
                // Redis <= 2.4
                $database = (int) $matches[1];
            }

            if (4 === $count) {
                // Redis >= 2.6
                $database = (int) $matches[2];
                $client = $matches[3];
            }

            return ' ';
        };

        $event = preg_replace_callback('/ \(db (\d+)\) | \[(\d+) (.*?)\] /', $callback, $event, 1);
        @list($timestamp, $command, $arguments) = explode(' ', $event, 3);

        return (object) array(
            'timestamp' => (float) $timestamp,
            'database'  => $database,
            'client'    => $client,
            'command'   => substr($command, 1, -1),
            'arguments' => $arguments,
        );
    }
}

/* --------------------------------------------------------------------------- */

namespace Predis\Replication;

use Predis\NotSupportedException;
use Predis\Command\CommandInterface;

/**
 * Defines a strategy for master/slave replication.
 *
 * @author Daniele Alessandri <suppakilla@gmail.com>
 */
class ReplicationStrategy
{
    protected $disallowed;
    protected $readonly;
    protected $readonlySHA1;

    /**
     *
     */
    public function __construct()
    {
        $this->disallowed = $this->getDisallowedOperations();
        $this->readonly = $this->getReadOnlyOperations();
        $this->readonlySHA1 = array();
    }

    /**
     * Returns if the specified command will perform a read-only operation
     * on Redis or not.
     *
     * @param CommandInterface $command Command instance.
     *
     * @return bool
     *
     * @throws NotSupportedException
     */
    public function isReadOperation(CommandInterface $command)
    {
        if (isset($this->disallowed[$id = $command->getId()])) {
            throw new NotSupportedException(
                "The command '$id' is not allowed in replication mode."
            );
        }

        if (isset($this->readonly[$id])) {
            if (true === $readonly = $this->readonly[$id]) {
                return true;
            }

            return call_user_func($readonly, $command);
        }

        if (($eval = $id === 'EVAL') || $id === 'EVALSHA') {
            $sha1 = $eval ? sha1($command->getArgument(0)) : $command->getArgument(0);

            if (isset($this->readonlySHA1[$sha1])) {
                if (true === $readonly = $this->readonlySHA1[$sha1]) {
                    return true;
                }

                return call_user_func($readonly, $command);
            }
        }

        return false;
    }

    /**
     * Returns if the specified command is not allowed for execution in a master
     * / slave replication context.
     *
     * @param CommandInterface $command Command instance.
     *
     * @return bool
     */
    public function isDisallowedOperation(CommandInterface $command)
    {
        return isset($this->disallowed[$command->getId()]);
    }

    /**
     * Checks if a SORT command is a readable operation by parsing the arguments
     * array of the specified commad instance.
     *
     * @param CommandInterface $command Command instance.
     *
     * @return bool
     */
    protected function isSortReadOnly(CommandInterface $command)
    {
        $arguments = $command->getArguments();

        return ($c = count($arguments)) === 1 ? true : $arguments[$c - 2] !== 'STORE';
    }

    /**
     * Marks a command as a read-only operation.
     *
     * When the behavior of a command can be decided only at runtime depending
     * on its arguments, a callable object can be provided to dynamically check
     * if the specified command performs a read or a write operation.
     *
     * @param string $commandID Command ID.
     * @param mixed  $readonly  A boolean value or a callable object.
     */
    public function setCommandReadOnly($commandID, $readonly = true)
    {
        $commandID = strtoupper($commandID);

        if ($readonly) {
            $this->readonly[$commandID] = $readonly;
        } else {
            unset($this->readonly[$commandID]);
        }
    }

    /**
     * Marks a Lua script for EVAL and EVALSHA as a read-only operation. When
     * the behaviour of a script can be decided only at runtime depending on
     * its arguments, a callable object can be provided to dynamically check
     * if the passed instance of EVAL or EVALSHA performs write operations or
     * not.
     *
     * @param string $script   Body of the Lua script.
     * @param mixed  $readonly A boolean value or a callable object.
     */
    public function setScriptReadOnly($script, $readonly = true)
    {
        $sha1 = sha1($script);

        if ($readonly) {
            $this->readonlySHA1[$sha1] = $readonly;
        } else {
            unset($this->readonlySHA1[$sha1]);
        }
    }

    /**
     * Returns the default list of disallowed commands.
     *
     * @return array
     */
    protected function getDisallowedOperations()
    {
        return array(
            'SHUTDOWN'          => true,
            'INFO'              => true,
            'DBSIZE'            => true,
            'LASTSAVE'          => true,
            'CONFIG'            => true,
            'MONITOR'           => true,
            'SLAVEOF'           => true,
            'SAVE'              => true,
            'BGSAVE'            => true,
            'BGREWRITEAOF'      => true,
            'SLOWLOG'           => true,
        );
    }

    /**
     * Returns the default list of commands performing read-only operations.
     *
     * @return array
     */
    protected function getReadOnlyOperations()
    {
        return array(
            'EXISTS'            => true,
            'TYPE'              => true,
            'KEYS'              => true,
            'SCAN'              => true,
            'RANDOMKEY'         => true,
            'TTL'               => true,
            'GET'               => true,
            'MGET'              => true,
            'SUBSTR'            => true,
            'STRLEN'            => true,
            'GETRANGE'          => true,
            'GETBIT'            => true,
            'LLEN'              => true,
            'LRANGE'            => true,
            'LINDEX'            => true,
            'SCARD'             => true,
            'SISMEMBER'         => true,
            'SINTER'            => true,
            'SUNION'            => true,
            'SDIFF'             => true,
            'SMEMBERS'          => true,
            'SSCAN'             => true,
            'SRANDMEMBER'       => true,
            'ZRANGE'            => true,
            'ZREVRANGE'         => true,
            'ZRANGEBYSCORE'     => true,
            'ZREVRANGEBYSCORE'  => true,
            'ZCARD'             => true,
            'ZSCORE'            => true,
            'ZCOUNT'            => true,
            'ZRANK'             => true,
            'ZREVRANK'          => true,
            'ZSCAN'             => true,
            'ZLEXCOUNT'         => true,
            'ZRANGEBYLEX'       => true,
            'HGET'              => true,
            'HMGET'             => true,
            'HEXISTS'           => true,
            'HLEN'              => true,
            'HKEYS'             => true,
            'HVALS'             => true,
            'HGETALL'           => true,
            'HSCAN'             => true,
            'PING'              => true,
            'AUTH'              => true,
            'SELECT'            => true,
            'ECHO'              => true,
            'QUIT'              => true,
            'OBJECT'            => true,
            'BITCOUNT'          => true,
            'TIME'              => true,
            'PFCOUNT'           => true,
            'SORT'              => array($this, 'isSortReadOnly'),
        );
    }
}

/* --------------------------------------------------------------------------- */
// phpcs:enable
