import weakref
import sys
from ..cursors import Cursor


class AsyncCursor(Cursor):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    async def __aenter__(self):
        return self

    async def __anext__(self):
        ret = await self.fetchone()
        if ret is not None:
            return ret
        else:
            raise StopAsyncIteration  # noqa

    async def __aexit__(self, exec, value, traceback):
        await self.close()

    async def close(self):
        '''
        Closing a cursor just exhausts all remaining data.
        '''
        if not self.connection:
            return
        try:
            while await self.nextset():
                pass
        except:
            pass

    async def _flush(self):
        if self._result:
            await self._result.read_rest_rowdata_packet()

    async def nextset(self):
        ''' Get the next query set '''
        if self._executed:
            await self.fetchall()
        del self.messages[:]

        if not self._result or not self._result.has_next:
            return None
        connection = self._get_db()
        await connection.next_result()
        self._do_get_result()
        return True

    async def execute(self, query, args=None):
        ''' Execute a query '''
        self._rowcount = None

        conn = self._get_db()
        if hasattr(conn, '_last_execute_cursor') and not conn._last_execute_cursor() is None:
            await conn._last_execute_cursor()._flush()

        del self.messages[:]

        if args is not None:
            if isinstance(args, (tuple, list)):
                escaped_args = tuple(conn.escape(arg) for arg in args)
            elif isinstance(args, dict):
                escaped_args = dict((key, conn.escape(val)) for (key, val) in args.items())
            else:
                # If it's not a dictionary let's try escaping it anyways.
                # Worst case it will throw a Value error
                escaped_args = conn.escape(args)

            query = query % escaped_args

        try:
            await self._query(query)
        except:
            exc, value, tb = sys.exc_info()
            del tb
            self.messages.append((exc, value))
            self.errorhandler(exc, value)

        self._executed = query
        conn._last_execute_cursor = weakref.ref(self)

    async def executemany(self, query, args):
        ''' Run several data against one query '''
        del self.messages[:]

        rowcount = 0
        for params in args:
            await self.execute(query, params)
            if self.rowcount != -1:
                rowcount += self.rowcount
        self._result = None
        self._rowcount = rowcount
        return rowcount

    async def callproc(self, procname, args=()):
        """Execute stored procedure procname with args

        procname -- string, name of procedure to execute on server

        args -- Sequence of parameters to use with procedure

        Returns the original args.

        Compatibility warning: PEP-249 specifies that any modified
        parameters must be returned. This is currently impossible
        as they are only available by storing them in a server
        variable and then retrieved by a query. Since stored
        procedures return zero or more result sets, there is no
        reliable way to get at OUT or INOUT parameters via callproc.
        The server variables are named @_procname_n, where procname
        is the parameter above and n is the position of the parameter
        (from zero). Once all result sets generated by the procedure
        have been fetched, you can issue a SELECT @_procname_0, ...
        query using .execute() to get any OUT or INOUT values.

        Compatibility warning: The act of calling a stored procedure
        itself creates an empty result set. This appears after any
        result sets generated by the procedure. This is non-standard
        behavior with respect to the DB-API. Be sure to use nextset()
        to advance through all result sets; otherwise you may get
        disconnected.
        """
        conn = self._get_db()
        for index, arg in enumerate(args):
            q = "SET @_%s_%d=%s" % (procname, index, conn.escape(arg))
            self._query(q)
            self.nextset()

        q = "CALL %s(%s)" % (procname,
                             ','.join(['@_%s_%d' % (procname, i)
                                       for i in range(len(args))]))
        self._query(q)
        self._executed = q

        return args

    async def fetchone(self):
        ''' Fetch the next row '''
        self._check_executed()
        if self._result is None:
            return None
        return await self._result.fetchone()

    async def fetchmany(self, size=None):
        ''' Fetch several rows '''
        self._check_executed()
        size = size or self.arraysize
        if self._result is None:
            return None
        result = []
        for i in range(size):
            r = await self._result.fetchone()
            if not r:
                break
            result.append(r)
        return result

    async def fetchall(self):
        ''' Fetch all the rows '''
        self._check_executed()
        if self._result is None:
            return None
        result = []

        r = await self._result.fetchone()
        while r:
            result.append(r)
            r = await self._result.fetchone()

        return result

    async def _query(self, q):
        conn = self._get_db()
        self._last_executed = q
        await conn.query(q)
        self._do_get_result()


class AsyncDictCursor(AsyncCursor):
    """A cursor which returns results as a dictionary"""

    async def execute(self, query, args=None):
        result = await super().execute(query, args)
        if self.description:
            self._fields = [field[0] for field in self.description]
        return result

    async def fetchone(self):
        ''' Fetch the next row '''
        self._check_executed()
        if self._result is None:
            return None
        r = await super().fetchone()
        if not r:
            return None
        return dict(zip(self._fields, r))

    async def fetchmany(self, size=None):
        ''' Fetch several rows '''
        self._check_executed()
        if self._result is None:
            return None
        result = [dict(zip(self._fields, r)) for r in await super().fetchmany(size)]
        return tuple(result)

    async def fetchall(self):
        ''' Fetch all the rows '''
        self._check_executed()
        if self._result is None:
            return None
        return tuple([
            dict(zip(self._fields, r)) for r in await super().fetchall()
        ])
