<?php

/*
 * This file is part of the Predis package.
 *
 * (c) 2009-2020 Daniele Alessandri
 * (c) 2021-2023 Till Krüss
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Predis\Command\Processor;

use InvalidArgumentException;
use Predis\Command\CommandInterface;
use Predis\Command\PrefixableCommandInterface;

/**
 * Command processor capable of prefixing keys stored in the arguments of Redis
 * commands supported.
 */
class KeyPrefixProcessor implements ProcessorInterface
{
    private $prefix;
    private $commands;

    /**
     * @param string $prefix Prefix for the keys.
     */
    public function __construct($prefix)
    {
        $this->prefix = $prefix;

        $prefixFirst = static::class . '::first';
        $prefixAll = static::class . '::all';
        $prefixInterleaved = static::class . '::interleaved';
        $prefixSkipFirst = static::class . '::skipFirst';
        $prefixSkipLast = static::class . '::skipLast';
        $prefixSort = static::class . '::sort';
        $prefixEvalKeys = static::class . '::evalKeys';
        $prefixZsetStore = static::class . '::zsetStore';
        $prefixMigrate = static::class . '::migrate';
        $prefixGeoradius = static::class . '::georadius';

        $this->commands = [
            /* ---------------- Redis 1.2 ---------------- */
            'EXISTS' => $prefixAll,
            'DEL' => $prefixAll,
            'TYPE' => $prefixFirst,
            'KEYS' => $prefixFirst,
            'RENAME' => $prefixAll,
            'RENAMENX' => $prefixAll,
            'EXPIRE' => $prefixFirst,
            'EXPIREAT' => $prefixFirst,
            'TTL' => $prefixFirst,
            'MOVE' => $prefixFirst,
            'SORT' => $prefixSort,
            'DUMP' => $prefixFirst,
            'RESTORE' => $prefixFirst,
            'SET' => $prefixFirst,
            'SETNX' => $prefixFirst,
            'MSET' => $prefixInterleaved,
            'MSETNX' => $prefixInterleaved,
            'GET' => $prefixFirst,
            'MGET' => $prefixAll,
            'GETSET' => $prefixFirst,
            'INCR' => $prefixFirst,
            'INCRBY' => $prefixFirst,
            'DECR' => $prefixFirst,
            'DECRBY' => $prefixFirst,
            'RPUSH' => $prefixFirst,
            'LPUSH' => $prefixFirst,
            'LLEN' => $prefixFirst,
            'LRANGE' => $prefixFirst,
            'LTRIM' => $prefixFirst,
            'LINDEX' => $prefixFirst,
            'LSET' => $prefixFirst,
            'LREM' => $prefixFirst,
            'LPOP' => $prefixFirst,
            'RPOP' => $prefixFirst,
            'RPOPLPUSH' => $prefixAll,
            'SADD' => $prefixFirst,
            'SREM' => $prefixFirst,
            'SPOP' => $prefixFirst,
            'SMOVE' => $prefixSkipLast,
            'SCARD' => $prefixFirst,
            'SISMEMBER' => $prefixFirst,
            'SINTER' => $prefixAll,
            'SINTERSTORE' => $prefixAll,
            'SUNION' => $prefixAll,
            'SUNIONSTORE' => $prefixAll,
            'SDIFF' => $prefixAll,
            'SDIFFSTORE' => $prefixAll,
            'SMEMBERS' => $prefixFirst,
            'SRANDMEMBER' => $prefixFirst,
            'ZADD' => $prefixFirst,
            'ZINCRBY' => $prefixFirst,
            'ZREM' => $prefixFirst,
            'ZRANGE' => $prefixFirst,
            'ZREVRANGE' => $prefixFirst,
            'ZRANGEBYSCORE' => $prefixFirst,
            'ZCARD' => $prefixFirst,
            'ZSCORE' => $prefixFirst,
            'ZREMRANGEBYSCORE' => $prefixFirst,
            /* ---------------- Redis 2.0 ---------------- */
            'SETEX' => $prefixFirst,
            'APPEND' => $prefixFirst,
            'SUBSTR' => $prefixFirst,
            'BLPOP' => $prefixSkipLast,
            'BRPOP' => $prefixSkipLast,
            'ZUNIONSTORE' => $prefixZsetStore,
            'ZINTERSTORE' => $prefixZsetStore,
            'ZCOUNT' => $prefixFirst,
            'ZRANK' => $prefixFirst,
            'ZREVRANK' => $prefixFirst,
            'ZREMRANGEBYRANK' => $prefixFirst,
            'HSET' => $prefixFirst,
            'HSETNX' => $prefixFirst,
            'HMSET' => $prefixFirst,
            'HINCRBY' => $prefixFirst,
            'HGET' => $prefixFirst,
            'HMGET' => $prefixFirst,
            'HDEL' => $prefixFirst,
            'HEXISTS' => $prefixFirst,
            'HLEN' => $prefixFirst,
            'HKEYS' => $prefixFirst,
            'HVALS' => $prefixFirst,
            'HGETALL' => $prefixFirst,
            'SUBSCRIBE' => $prefixAll,
            'UNSUBSCRIBE' => $prefixAll,
            'PSUBSCRIBE' => $prefixAll,
            'PUNSUBSCRIBE' => $prefixAll,
            'PUBLISH' => $prefixFirst,
            /* ---------------- Redis 2.2 ---------------- */
            'PERSIST' => $prefixFirst,
            'STRLEN' => $prefixFirst,
            'SETRANGE' => $prefixFirst,
            'GETRANGE' => $prefixFirst,
            'SETBIT' => $prefixFirst,
            'GETBIT' => $prefixFirst,
            'RPUSHX' => $prefixFirst,
            'LPUSHX' => $prefixFirst,
            'LINSERT' => $prefixFirst,
            'BRPOPLPUSH' => $prefixSkipLast,
            'ZREVRANGEBYSCORE' => $prefixFirst,
            'WATCH' => $prefixAll,
            /* ---------------- Redis 2.6 ---------------- */
            'PTTL' => $prefixFirst,
            'PEXPIRE' => $prefixFirst,
            'PEXPIREAT' => $prefixFirst,
            'PSETEX' => $prefixFirst,
            'INCRBYFLOAT' => $prefixFirst,
            'BITOP' => $prefixSkipFirst,
            'BITCOUNT' => $prefixFirst,
            'HINCRBYFLOAT' => $prefixFirst,
            'EVAL' => $prefixEvalKeys,
            'EVALSHA' => $prefixEvalKeys,
            'MIGRATE' => $prefixMigrate,
            /* ---------------- Redis 2.8 ---------------- */
            'SSCAN' => $prefixFirst,
            'ZSCAN' => $prefixFirst,
            'HSCAN' => $prefixFirst,
            'PFADD' => $prefixFirst,
            'PFCOUNT' => $prefixAll,
            'PFMERGE' => $prefixAll,
            'ZLEXCOUNT' => $prefixFirst,
            'ZRANGEBYLEX' => $prefixFirst,
            'ZREMRANGEBYLEX' => $prefixFirst,
            'ZREVRANGEBYLEX' => $prefixFirst,
            'BITPOS' => $prefixFirst,
            /* ---------------- Redis 3.2 ---------------- */
            'HSTRLEN' => $prefixFirst,
            'BITFIELD' => $prefixFirst,
            'GEOADD' => $prefixFirst,
            'GEOHASH' => $prefixFirst,
            'GEOPOS' => $prefixFirst,
            'GEODIST' => $prefixFirst,
            'GEORADIUS' => $prefixGeoradius,
            'GEORADIUSBYMEMBER' => $prefixGeoradius,
            /* ---------------- Redis 5.0 ---------------- */
            'XADD' => $prefixFirst,
            'XRANGE' => $prefixFirst,
            'XREVRANGE' => $prefixFirst,
            'XDEL' => $prefixFirst,
            'XLEN' => $prefixFirst,
            'XACK' => $prefixFirst,
            'XTRIM' => $prefixFirst,

            /* ---------------- Redis 6.2 ---------------- */
            'GETDEL' => $prefixFirst,

            /* ---------------- Redis 7.0 ---------------- */
            'EXPIRETIME' => $prefixFirst,

            /* RedisJSON */
            'JSON.ARRAPPEND' => $prefixFirst,
            'JSON.ARRINDEX' => $prefixFirst,
            'JSON.ARRINSERT' => $prefixFirst,
            'JSON.ARRLEN' => $prefixFirst,
            'JSON.ARRPOP' => $prefixFirst,
            'JSON.ARRTRIM' => $prefixFirst,
            'JSON.CLEAR' => $prefixFirst,
            'JSON.DEBUG MEMORY' => $prefixFirst,
            'JSON.DEL' => $prefixFirst,
            'JSON.FORGET' => $prefixFirst,
            'JSON.GET' => $prefixFirst,
            'JSON.MGET' => $prefixAll,
            'JSON.NUMINCRBY' => $prefixFirst,
            'JSON.OBJKEYS' => $prefixFirst,
            'JSON.OBJLEN' => $prefixFirst,
            'JSON.RESP' => $prefixFirst,
            'JSON.SET' => $prefixFirst,
            'JSON.STRAPPEND' => $prefixFirst,
            'JSON.STRLEN' => $prefixFirst,
            'JSON.TOGGLE' => $prefixFirst,
            'JSON.TYPE' => $prefixFirst,

            /* RedisBloom */
            'BF.ADD' => $prefixFirst,
            'BF.EXISTS' => $prefixFirst,
            'BF.INFO' => $prefixFirst,
            'BF.INSERT' => $prefixFirst,
            'BF.LOADCHUNK' => $prefixFirst,
            'BF.MADD' => $prefixFirst,
            'BF.MEXISTS' => $prefixFirst,
            'BF.RESERVE' => $prefixFirst,
            'BF.SCANDUMP' => $prefixFirst,
            'CF.ADD' => $prefixFirst,
            'CF.ADDNX' => $prefixFirst,
            'CF.COUNT' => $prefixFirst,
            'CF.DEL' => $prefixFirst,
            'CF.EXISTS' => $prefixFirst,
            'CF.INFO' => $prefixFirst,
            'CF.INSERT' => $prefixFirst,
            'CF.INSERTNX' => $prefixFirst,
            'CF.LOADCHUNK' => $prefixFirst,
            'CF.MEXISTS' => $prefixFirst,
            'CF.RESERVE' => $prefixFirst,
            'CF.SCANDUMP' => $prefixFirst,
            'CMS.INCRBY' => $prefixFirst,
            'CMS.INFO' => $prefixFirst,
            'CMS.INITBYDIM' => $prefixFirst,
            'CMS.INITBYPROB' => $prefixFirst,
            'CMS.QUERY' => $prefixFirst,
            'TDIGEST.ADD' => $prefixFirst,
            'TDIGEST.BYRANK' => $prefixFirst,
            'TDIGEST.BYREVRANK' => $prefixFirst,
            'TDIGEST.CDF' => $prefixFirst,
            'TDIGEST.CREATE' => $prefixFirst,
            'TDIGEST.INFO' => $prefixFirst,
            'TDIGEST.MAX' => $prefixFirst,
            'TDIGEST.MIN' => $prefixFirst,
            'TDIGEST.QUANTILE' => $prefixFirst,
            'TDIGEST.RANK' => $prefixFirst,
            'TDIGEST.RESET' => $prefixFirst,
            'TDIGEST.REVRANK' => $prefixFirst,
            'TDIGEST.TRIMMED_MEAN' => $prefixFirst,
            'TOPK.ADD' => $prefixFirst,
            'TOPK.INCRBY' => $prefixFirst,
            'TOPK.INFO' => $prefixFirst,
            'TOPK.LIST' => $prefixFirst,
            'TOPK.QUERY' => $prefixFirst,
            'TOPK.RESERVE' => $prefixFirst,

            /* RediSearch */
            'FT.AGGREGATE' => $prefixFirst,
            'FT.ALTER' => $prefixFirst,
            'FT.CREATE' => $prefixFirst,
            'FT.CURSOR DEL' => $prefixFirst,
            'FT.CURSOR READ' => $prefixFirst,
            'FT.DROPINDEX' => $prefixFirst,
            'FT.EXPLAIN' => $prefixFirst,
            'FT.INFO' => $prefixFirst,
            'FT.PROFILE' => $prefixFirst,
            'FT.SEARCH' => $prefixFirst,
            'FT.SPELLCHECK' => $prefixFirst,
            'FT.SYNDUMP' => $prefixFirst,
            'FT.SYNUPDATE' => $prefixFirst,
            'FT.TAGVALS' => $prefixFirst,

            /* Redis TimeSeries */
            'TS.ADD' => $prefixFirst,
            'TS.ALTER' => $prefixFirst,
            'TS.CREATE' => $prefixFirst,
            'TS.DECRBY' => $prefixFirst,
            'TS.DEL' => $prefixFirst,
            'TS.GET' => $prefixFirst,
            'TS.INCRBY' => $prefixFirst,
            'TS.INFO' => $prefixFirst,
            'TS.MGET' => $prefixFirst,
            'TS.MRANGE' => $prefixFirst,
            'TS.MREVRANGE' => $prefixFirst,
            'TS.QUERYINDEX' => $prefixFirst,
            'TS.RANGE' => $prefixFirst,
            'TS.REVRANGE' => $prefixFirst,
        ];
    }

    /**
     * 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())])) {
            $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 (strtoupper($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);
        }
    }

    /**
     * Applies the specified prefix to the key of a MIGRATE command.
     *
     * @param CommandInterface $command Command instance.
     * @param string           $prefix  Prefix string.
     */
    public static function migrate(CommandInterface $command, $prefix)
    {
        if ($arguments = $command->getArguments()) {
            $arguments[2] = "$prefix{$arguments[2]}";
            $command->setRawArguments($arguments);
        }
    }

    /**
     * Applies the specified prefix to the key of a GEORADIUS command.
     *
     * @param CommandInterface $command Command instance.
     * @param string           $prefix  Prefix string.
     */
    public static function georadius(CommandInterface $command, $prefix)
    {
        if ($arguments = $command->getArguments()) {
            $arguments[0] = "$prefix{$arguments[0]}";
            $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4;

            if (($count = count($arguments)) > $startIndex) {
                for ($i = $startIndex; $i < $count; ++$i) {
                    switch (strtoupper($arguments[$i])) {
                        case 'STORE':
                        case 'STOREDIST':
                            $arguments[$i] = "$prefix{$arguments[++$i]}";
                            break;
                    }
                }
            }

            $command->setRawArguments($arguments);
        }
    }
}
