"""A wait callback to allow psycopg2 cooperation with gevent.

Use `patch_psycopg()` to enable gevent support in Psycopg.
"""

# Copyright (C) 2010-2012 Daniele Varrazzo <daniele.varrazzo@gmail.com>
# All rights reserved.  See COPYING file for details.


from __future__ import absolute_import

import psycopg2
from psycopg2.extras import RealDictConnection
from psycopg2 import extensions

from gevent.coros import Semaphore
from gevent.local import local as gevent_local
from gevent.socket import wait_read, wait_write


def patch_psycopg():
    """Configure Psycopg to be used with gevent in non-blocking way."""
    if not hasattr(extensions, 'set_wait_callback'):
        raise ImportError(
            "support for coroutines not available in this Psycopg version (%s)"
            % psycopg2.__version__)

    extensions.set_wait_callback(gevent_wait_callback)

def gevent_wait_callback(conn, timeout=None):
    """A wait callback useful to allow gevent to work with Psycopg."""
    while 1:
        state = conn.poll()
        if state == extensions.POLL_OK:
            break
        elif state == extensions.POLL_READ:
            wait_read(conn.fileno(), timeout=timeout)
        elif state == extensions.POLL_WRITE:
            wait_write(conn.fileno(), timeout=timeout)
        else:
            raise psycopg2.OperationalError(
                "Bad result from poll: %r" % state)


class ConnectionPool(object):

    def __init__(self, dsn, max_con=10, max_idle=3,
                 connection_factory=RealDictConnection):
        self.dsn = dsn
        self.max_con = max_con
        self.max_idle = max_idle
        self.connection_factory = connection_factory
        self._sem = Semaphore(max_con)
        self._free = []
        self._local = gevent_local()

    def __enter__(self):
        self._sem.acquire()
        try:
            if getattr(self._local, 'con', None) is not None:
                raise RuntimeError("Attempting to re-enter connection pool?")
            if self._free:
                con = self._free.pop()
            else:
                con = psycopg2.connect(
                    dsn=self.dsn, connection_factory=self.connection_factory)
            self._local.con = con
            return con
        except StandardError:
            self._sem.release()
            raise

    def __exit__(self, exc_type, exc_value, traceback):
        try:
            if self._local.con is None:
                raise RuntimeError("Exit connection pool with no connection?")
            if exc_type is not None:
                self.rollback()
            else:
                self.commit()
            if len(self._free) < self.max_idle:
                self._free.append(self._local.con)
            self._local.con = None
        finally:
            self._sem.release()

    def commit(self):
        self._local.con.commit()

    def rollback(self):
        self._local.con.rollback()
