import pickle
import redis


class RedisClient:
    def __init__(self, redis_host, redis_port):
        self.redis_client = redis.Redis(host=redis_host, port=redis_port)

    ##########
    # Getters
    ##########
    def get(self, key, default=None):
        """
        Retrieve the byte value. If key does not exist, return default.
        """
        cached_instance = self.redis_client.get(key)
        if cached_instance:
            return pickle.loads(cached_instance)
        else:
            return default

    def mget(self, keys, default=None):
        """
        Get the values of multiple keys at once. If the key does not exists,
        default value is returned instead.
        """
        response_list = self.redis_client.mget(keys)
        return [pickle.loads(res) or default for res in response_list]

    def fetch(self, key, func, ttl, *args, **kwargs):
        """
        Wrapper for Redis GET function, to help get the value
        to the key being passed in, if the key does not exist,
        call the callback function being passed in instead
        """
        res = self.redis_client.get(key)
        if not res:
            res = func(*args, **kwargs)
            if res:
                res = pickle.dumps(res)
                self.redis_client.set(key, res, ttl)
        return pickle.loads(res)

    def exists(self, key):
        """
        Return 1 if the key exists.
        Return 0 if the key does not exist.
        """
        return self.redis_client.exists(key)

    ##########
    # Setters
    ##########
    def set(self, key, value, expiry=None):
        """
        Set key to hold the byte value. If key already holds a value,
        it is overwritten, regardless of its type. Any previous time
        to live associated with the key is discarded on successful
        SET operation.
        """
        serialized_value = pickle.dumps(value)
        self.redis_client.set(key, serialized_value, expiry)

    def mset(self, mapping):
        """
        Set multiple {key: value} pairs at once. Existing keys
        are overwritten, ttls are discarded.
        """
        if mapping:
            for key in mapping:
                mapping[key] = pickle.dumps(mapping[key])
            self.redis_client.mset(mapping)

    ##########
    # List Operations
    ##########
    def list_rpush(self, key, values, expiry=None):
        if not values:
            return 0

        if isinstance(values, list):
            serialized_values = [pickle.dumps(value) for value in values]
            self.redis_client.rpush(key, *serialized_values)
        else:
            serialized_value = pickle.dumps(values)
            self.redis_client.rpush(key, serialized_value)

        # Sets the expiry on the entire list, not the elements
        if expiry:
            self.redis_client.expire(key, expiry)

    def list_lpop(self, key):
        """
        Removes and returns the first element of the list stored at key.
        """
        res = self.redis_client.lpop(key)
        if res:
            return pickle.loads(res)
        else:
            return None

    def llen(self, key):
        """
        Returns the length of the list stored at key. If key does not
        exist, it is interpreted as an empty list and 0 is returned.
        An error is returned when the value stored at key is not a list.
        """
        return self.redis_client.llen(key)

    def list_range(self, key, start=0, stop=-1, default=[]):
        range = self.redis_client.lrange(key, start, stop)
        return [pickle.loads(res) or default for res in range]

    ##########
    # Deletion/Expiry
    ##########
    def delete(self, key):
        self.redis_client.delete(key)

    def flushall(self, asynchronous=False):
        """
        Flush all keys - clear cache
        """
        return self.redis_client.flushall(asynchronous)

    def expire(self, key, expiry):
        """
        Set a timeout on key. After the timeout has expired, the key will
        automatically be deleted. A key with an associated timeout is often
        said to be volatile in Redis terminology.
        It is possible to call EXPIRE using as argument a key that already
        has an existing expire set. In this case the time to live of a key
        is updated to the new value.
        Return value:
        1 if the timeout was set.
        0 if key does not exist.
        """
        return self.redis_client.expire(key, expiry)

    def ttl(self, key):
        """
        Returns the remaining time to live of a key that has a timeout.
        This introspection capability allows a Redis client to check how
        many seconds a given key will continue to be part of the dataset.
        The command returns -2 if the key does not exist.
        The command returns -1 if the key exists but has no associated expire.
        """
        return self.redis_client.ttl(key)
