from collections import namedtuple

from aioredis.util import wait_convert, _NOTSET


GeoPoint = namedtuple('GeoPoint', ('longitude', 'latitude'))
GeoMember = namedtuple('GeoMember', ('member', 'dist', 'hash', 'coord'))


class GeoCommandsMixin:
    """Geo commands mixin.

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

    def geoadd(self, key, longitude, latitude, member, *args, **kwargs):
        """Add one or more geospatial items in the geospatial index represented
        using a sorted set.

        :rtype: int
        """
        return self.execute(
            b'GEOADD', key, longitude, latitude, member, *args, **kwargs
        )

    def geohash(self, key, member, *members, **kwargs):
        """Returns members of a geospatial index as standard geohash strings.

        :rtype: list[str or bytes or None]
        """
        return self.execute(
            b'GEOHASH', key, member, *members, **kwargs
        )

    def geopos(self, key, member, *members, **kwargs):
        """Returns longitude and latitude of members of a geospatial index.

        :rtype: list[GeoPoint or None]
        """
        fut = self.execute(b'GEOPOS', key, member, *members, **kwargs)
        return wait_convert(fut, make_geopos)

    def geodist(self, key, member1, member2, unit='m'):
        """Returns the distance between two members of a geospatial index.

        :rtype: list[float or None]
        """
        fut = self.execute(b'GEODIST', key, member1, member2, unit)
        return wait_convert(fut, make_geodist)

    def georadius(self, key, longitude, latitude, radius, unit='m', *,
                  with_dist=False, with_hash=False, with_coord=False,
                  count=None, sort=None, encoding=_NOTSET):
        """Query a sorted set representing a geospatial index to fetch members
        matching a given maximum distance from a point.

        Return value follows Redis convention:

        * if none of ``WITH*`` flags are set -- list of strings returned:

            >>> await redis.georadius('Sicily', 15, 37, 200, 'km')
            [b"Palermo", b"Catania"]

        * if any flag (or all) is set -- list of named tuples returned:

            >>> await redis.georadius('Sicily', 15, 37, 200, 'km',
            ...                       with_dist=True)
            [GeoMember(name=b"Palermo", dist=190.4424, hash=None, coord=None),
             GeoMember(name=b"Catania", dist=56.4413, hash=None, coord=None)]

        :raises TypeError: radius is not float or int
        :raises TypeError: count is not int
        :raises ValueError: if unit not equal ``m``, ``km``, ``mi`` or ``ft``
        :raises ValueError: if sort not equal ``ASC`` or ``DESC``

        :rtype: list[str] or list[GeoMember]
        """
        args = validate_georadius_options(
            radius, unit, with_dist, with_hash, with_coord, count, sort
        )

        fut = self.execute(
            b'GEORADIUS', key, longitude, latitude, radius,
            unit, *args, encoding=encoding
        )
        if with_dist or with_hash or with_coord:
            return wait_convert(fut, make_geomember,
                                with_dist=with_dist,
                                with_hash=with_hash,
                                with_coord=with_coord)
        return fut

    def georadiusbymember(self, key, member, radius, unit='m', *,
                          with_dist=False, with_hash=False, with_coord=False,
                          count=None, sort=None, encoding=_NOTSET):
        """Query a sorted set representing a geospatial index to fetch members
        matching a given maximum distance from a member.

        Return value follows Redis convention:

        * if none of ``WITH*`` flags are set -- list of strings returned:

            >>> await redis.georadiusbymember('Sicily', 'Palermo', 200, 'km')
            [b"Palermo", b"Catania"]

        * if any flag (or all) is set -- list of named tuples returned:

            >>> await redis.georadiusbymember('Sicily', 'Palermo', 200, 'km',
            ...                               with_dist=True)
            [GeoMember(name=b"Palermo", dist=190.4424, hash=None, coord=None),
             GeoMember(name=b"Catania", dist=56.4413, hash=None, coord=None)]

        :raises TypeError: radius is not float or int
        :raises TypeError: count is not int
        :raises ValueError: if unit not equal ``m``, ``km``, ``mi`` or ``ft``
        :raises ValueError: if sort not equal ``ASC`` or ``DESC``

        :rtype: list[str] or list[GeoMember]
        """
        args = validate_georadius_options(
            radius, unit, with_dist, with_hash, with_coord, count, sort
        )

        fut = self.execute(
            b'GEORADIUSBYMEMBER', key, member, radius,
            unit, *args, encoding=encoding)
        if with_dist or with_hash or with_coord:
            return wait_convert(fut, make_geomember,
                                with_dist=with_dist,
                                with_hash=with_hash,
                                with_coord=with_coord)
        return fut


def validate_georadius_options(radius, unit, with_dist, with_hash, with_coord,
                               count, sort):
    args = []

    if with_dist:
        args.append(b'WITHDIST')
    if with_hash:
        args.append(b'WITHHASH')
    if with_coord:
        args.append(b'WITHCOORD')

    if unit not in ['m', 'km', 'mi', 'ft']:
        raise ValueError("unit argument must be 'm', 'km', 'mi' or 'ft'")
    if not isinstance(radius, (int, float)):
        raise TypeError("radius argument must be int or float")
    if count:
        if not isinstance(count, int):
            raise TypeError("count argument must be int")
        args += [b'COUNT', count]
    if sort:
        if sort not in ['ASC', 'DESC']:
            raise ValueError("sort argument must be euqal 'ASC' or 'DESC'")
        args.append(sort)
    return args


def make_geocoord(value):
    if isinstance(value, list):
        return GeoPoint(*map(float, value))
    return value


def make_geodist(value):
    if value:
        return float(value)
    return value


def make_geopos(value):
    return [make_geocoord(val) for val in value]


def make_geomember(value, with_dist, with_coord, with_hash):
    res_rows = []

    for row in value:
        name = row.pop(0)
        dist = hash_ = coord = None
        if with_dist:
            dist = float(row.pop(0))
        if with_hash:
            hash_ = int(row.pop(0))
        if with_coord:
            coord = GeoPoint(*map(float, row.pop(0)))

        res_rows.append(GeoMember(name, dist, hash_, coord))

    return res_rows
