from aioredis.util import wait_convert, wait_ok, _NOTSET, _ScanIter


class GenericCommandsMixin:
    """Generic commands mixin.

    For commands details see: http://redis.io/commands/#generic
    """

    def delete(self, key, *keys):
        """Delete a key."""
        fut = self.execute(b'DEL', key, *keys)
        return wait_convert(fut, int)

    def dump(self, key):
        """Dump a key."""
        return self.execute(b'DUMP', key)

    def exists(self, key, *keys):
        """Check if key(s) exists.

        .. versionchanged:: v0.2.9
           Accept multiple keys; **return** type **changed** from bool to int.
        """
        return self.execute(b'EXISTS', key, *keys)

    def expire(self, key, timeout):
        """Set a timeout on key.

        if timeout is float it will be multiplied by 1000
        coerced to int and passed to `pexpire` method.

        Otherwise raises TypeError if timeout argument is not int.
        """
        if isinstance(timeout, float):
            return self.pexpire(key, int(timeout * 1000))
        if not isinstance(timeout, int):
            raise TypeError(
                "timeout argument must be int, not {!r}".format(timeout))
        fut = self.execute(b'EXPIRE', key, timeout)
        return wait_convert(fut, bool)

    def expireat(self, key, timestamp):
        """Set expire timestamp on a key.

        if timeout is float it will be multiplied by 1000
        coerced to int and passed to `pexpireat` method.

        Otherwise raises TypeError if timestamp argument is not int.
        """
        if isinstance(timestamp, float):
            return self.pexpireat(key, int(timestamp * 1000))
        if not isinstance(timestamp, int):
            raise TypeError("timestamp argument must be int, not {!r}"
                            .format(timestamp))
        fut = self.execute(b'EXPIREAT', key, timestamp)
        return wait_convert(fut, bool)

    def keys(self, pattern, *, encoding=_NOTSET):
        """Returns all keys matching pattern."""
        return self.execute(b'KEYS', pattern, encoding=encoding)

    def migrate(self, host, port, key, dest_db, timeout, *,
                copy=False, replace=False):
        """Atomically transfer a key from a Redis instance to another one."""
        if not isinstance(host, str):
            raise TypeError("host argument must be str")
        if not isinstance(timeout, int):
            raise TypeError("timeout argument must be int")
        if not isinstance(dest_db, int):
            raise TypeError("dest_db argument must be int")
        if not host:
            raise ValueError("Got empty host")
        if dest_db < 0:
            raise ValueError("dest_db must be greater equal 0")
        if timeout < 0:
            raise ValueError("timeout must be greater equal 0")

        flags = []
        if copy:
            flags.append(b'COPY')
        if replace:
            flags.append(b'REPLACE')
        fut = self.execute(b'MIGRATE', host, port,
                           key, dest_db, timeout, *flags)
        return wait_ok(fut)

    def migrate_keys(self, host, port, keys, dest_db, timeout, *,
                     copy=False, replace=False):
        """Atomically transfer keys from one Redis instance to another one.

        Keys argument must be list/tuple of keys to migrate.
        """
        if not isinstance(host, str):
            raise TypeError("host argument must be str")
        if not isinstance(timeout, int):
            raise TypeError("timeout argument must be int")
        if not isinstance(dest_db, int):
            raise TypeError("dest_db argument must be int")
        if not isinstance(keys, (list, tuple)):
            raise TypeError("keys argument must be list or tuple")
        if not host:
            raise ValueError("Got empty host")
        if dest_db < 0:
            raise ValueError("dest_db must be greater equal 0")
        if timeout < 0:
            raise ValueError("timeout must be greater equal 0")
        if not keys:
            raise ValueError("keys must not be empty")

        flags = []
        if copy:
            flags.append(b'COPY')
        if replace:
            flags.append(b'REPLACE')
        flags.append(b'KEYS')
        flags.extend(keys)
        fut = self.execute(b'MIGRATE', host, port,
                           "", dest_db, timeout, *flags)
        return wait_ok(fut)

    def move(self, key, db):
        """Move key from currently selected database to specified destination.

        :raises TypeError: if db is not int
        :raises ValueError: if db is less than 0
        """
        if not isinstance(db, int):
            raise TypeError("db argument must be int, not {!r}".format(db))
        if db < 0:
            raise ValueError("db argument must be not less than 0, {!r}"
                             .format(db))
        fut = self.execute(b'MOVE', key, db)
        return wait_convert(fut, bool)

    def object_refcount(self, key):
        """Returns the number of references of the value associated
        with the specified key (OBJECT REFCOUNT).
        """
        return self.execute(b'OBJECT', b'REFCOUNT', key)

    def object_encoding(self, key):
        """Returns the kind of internal representation used in order
        to store the value associated with a key (OBJECT ENCODING).
        """
        return self.execute(b'OBJECT', b'ENCODING', key, encoding='utf-8')

    def object_idletime(self, key):
        """Returns the number of seconds since the object is not requested
        by read or write operations (OBJECT IDLETIME).
        """
        return self.execute(b'OBJECT', b'IDLETIME', key)

    def persist(self, key):
        """Remove the existing timeout on key."""
        fut = self.execute(b'PERSIST', key)
        return wait_convert(fut, bool)

    def pexpire(self, key, timeout):
        """Set a milliseconds timeout on key.

        :raises TypeError: if timeout is not int
        """
        if not isinstance(timeout, int):
            raise TypeError("timeout argument must be int, not {!r}"
                            .format(timeout))
        fut = self.execute(b'PEXPIRE', key, timeout)
        return wait_convert(fut, bool)

    def pexpireat(self, key, timestamp):
        """Set expire timestamp on key, timestamp in milliseconds.

        :raises TypeError: if timeout is not int
        """
        if not isinstance(timestamp, int):
            raise TypeError("timestamp argument must be int, not {!r}"
                            .format(timestamp))
        fut = self.execute(b'PEXPIREAT', key, timestamp)
        return wait_convert(fut, bool)

    def pttl(self, key):
        """Returns time-to-live for a key, in milliseconds.

        Special return values (starting with Redis 2.8):

        * command returns -2 if the key does not exist.
        * command returns -1 if the key exists but has no associated expire.
        """
        # TODO: maybe convert negative values to:
        #       -2 to None  - no key
        #       -1 to False - no expire
        return self.execute(b'PTTL', key)

    def randomkey(self, *, encoding=_NOTSET):
        """Return a random key from the currently selected database."""
        return self.execute(b'RANDOMKEY', encoding=encoding)

    def rename(self, key, newkey):
        """Renames key to newkey.

        :raises ValueError: if key == newkey
        """
        if key == newkey:
            raise ValueError("key and newkey are the same")
        fut = self.execute(b'RENAME', key, newkey)
        return wait_ok(fut)

    def renamenx(self, key, newkey):
        """Renames key to newkey only if newkey does not exist.

        :raises ValueError: if key == newkey
        """
        if key == newkey:
            raise ValueError("key and newkey are the same")
        fut = self.execute(b'RENAMENX', key, newkey)
        return wait_convert(fut, bool)

    def restore(self, key, ttl, value):
        """Creates a key associated with a value that is obtained via DUMP."""
        return self.execute(b'RESTORE', key, ttl, value)

    def scan(self, cursor=0, match=None, count=None):
        """Incrementally iterate the keys space.

        Usage example:

        >>> match = 'something*'
        >>> cur = b'0'
        >>> while cur:
        ...     cur, keys = await redis.scan(cur, match=match)
        ...     for key in keys:
        ...         print('Matched:', key)

        """
        args = []
        if match is not None:
            args += [b'MATCH', match]
        if count is not None:
            args += [b'COUNT', count]
        fut = self.execute(b'SCAN', cursor, *args)
        return wait_convert(fut, lambda o: (int(o[0]), o[1]))

    def iscan(self, *, match=None, count=None):
        """Incrementally iterate the keys space using async for.

        Usage example:

        >>> async for key in redis.iscan(match='something*'):
        ...     print('Matched:', key)

        """
        return _ScanIter(lambda cur: self.scan(cur,
                                               match=match, count=count))

    def sort(self, key, *get_patterns,
             by=None, offset=None, count=None,
             asc=None, alpha=False, store=None):
        """Sort the elements in a list, set or sorted set."""
        args = []
        if by is not None:
            args += [b'BY', by]
        if offset is not None and count is not None:
            args += [b'LIMIT', offset, count]
        if get_patterns:
            args += sum(([b'GET', pattern] for pattern in get_patterns), [])
        if asc is not None:
            args += [asc is True and b'ASC' or b'DESC']
        if alpha:
            args += [b'ALPHA']
        if store is not None:
            args += [b'STORE', store]
        return self.execute(b'SORT', key, *args)

    def touch(self, key, *keys):
        """Alters the last access time of a key(s).

        Returns the number of keys that were touched.
        """
        return self.execute(b'TOUCH', key, *keys)

    def ttl(self, key):
        """Returns time-to-live for a key, in seconds.

        Special return values (starting with Redis 2.8):
        * command returns -2 if the key does not exist.
        * command returns -1 if the key exists but has no associated expire.
        """
        # TODO: maybe convert negative values to:
        #       -2 to None  - no key
        #       -1 to False - no expire
        return self.execute(b'TTL', key)

    def type(self, key):
        """Returns the string representation of the value's type stored at key.
        """
        # NOTE: for non-existent keys TYPE returns b'none'
        return self.execute(b'TYPE', key)

    def unlink(self, key, *keys):
        """Delete a key asynchronously in another thread."""
        return wait_convert(self.execute(b'UNLINK', key, *keys), int)

    def wait(self, numslaves, timeout):
        """Wait for the synchronous replication of all the write
        commands sent in the context of the current connection.
        """
        return self.execute(b'WAIT', numslaves, timeout)
