"""Durus database interface for Pylons.

Usage:

    # In your_pylons_project.lib.base
    from pylons_durus import get_connection
    class BaseController(WSGIController):
        def __call__(self, environ, start_response):
            self.db_connection = get_connection("durus.main")
            self.db_connection.abort()  # Roll back any uncommitted transaction.
            return WSGIController(self, environ, start_response)

The user needs an entry in the "[app.main]" section of their config file:

    # TCP connection to Durus server.
    durus.main = client://localhost:5001

    # Unix domain socket to Durus server.
    durus.main = unix:///my_db.sock
    durus.main = unix:////var/run/my_db.sock

    # Open Durus database directly.  !!! SAFE ONLY IF readonly=1 !!!
    durus.main = file:///relative/path/from/config/file/my_db.durus?readonly=1
    durus.main = file:////absolute/path/needs/four/slashes/db.durus?readonly=1

Three slashes name a path relative to the config file.  Four slashes name an
absolute path.  This was chosen to be consistent with SQLAlchemy's SQLite
behavior, and because I'm using sqlalchemy.engine.make_url() to parse the URL.

Query parameters for all schemes:
    cache_size : int (default 1000000).
        Number of Persistent objects to cache.  NOT byte size of cache.

Query parameters for file: scheme:
    readony: 1 or 0 (default 0).  Open the database read-only.  USE 1!!!
    repair: 1 or 0 (default 0).  Repair the database on open if corrupted.

The connections are stored in a thread-local dict keyed by URL.
An application may connect to multiple databases by specifying a different
config entry for each (e.g., "durus.connection2").

An application may connect to multiple databases by specifying a different
config key and URL for each.  The connections themselves are stored in a
thread-local dict keyed by URL, so identical URLs map to the same connection.
"""
import os
import thread
import threading

from durus.client_storage import ClientStorage
from durus.connection import Connection
from durus.file_storage import FileStorage
from durus.storage_server import recv, sendall
from durus.storage_server import StorageServer
from paste.deploy import CONFIG
from sqlalchemy.engine.url import make_url as make_U

__all__ = ["get_connection"]

localdata = None

def get_connection(config_key):
    global localdata
    if localdata is None:
        localdata = threading.local()
    if not hasattr(localdata, "connections"):
        localdata.connections = {}  
        # 'connections' is a thread-local dict of URL : Durus connection.
    err_suffix = "config key '%s'" % config_key
    url = CONFIG["app_conf"][config_key]  # Raises KeyError.
    conn = None
    if url in localdata.connections:
        conn = localdata.connections[url]
        if not is_connected(conn):
            conn = None
    if conn is None:
        conn = create_connection(url)
        localdata.connections[url] = conn
    #print "Thread %s using %s" % (thread.get_ident(), conn)
    return conn

def is_connected(conn):
    """Is the connection still alive?"""
    if not isinstance(conn.storage, ClientStorage):
        return True
    # Based on durus.client_storage.ClientStorage.__init__()
    protocol = StorageServer.protocol
    assert len(protocol) == 4
    try:
        sendall(conn.storage.s, "V" + protocol)
        recv(conn.storage.s, 4)  # Discard response.
    except IOError:
        return False
    return True

def create_connection(url):
    """Return a new Durus connection based on the URL."""
    paths_relative_to = os.path.dirname(CONFIG["global_conf"]["__file__"])
    U = make_U(url)
    database = U.database
    if paths_relative_to is not None and database is not None:
        database = os.path.join(paths_relative_to, U.database)
    cache_size = int(U.query.pop("cache_size", 1000000))
    if U.drivername == "file":
        readonly = bool(int(U.query.pop("readonly", 0)))
        repair = bool(int(U.query.pop("repair", 0)))
        storage = FileStorage(database, readonly, repair)   
    elif U.drivername == "client":
        storage = ClientStorage(U.host, U.port)
    elif U.drivername == "unix":  # Unix domain socket.
        storage = ClientStorage(address=database)
    else:
        msg = "unknown Durus scheme '%s' %s"
        tup = U.drivername, err_suffix
        raise ValueError(msg % tup)
    if U.query:
        msg = "illegal query parameters for '%s' database connection %s: %s"
        tup = U.drivername, err_suffix, ", ".join(U.query.iterkeys())
        raise ValueError(msg % tup)
    return Connection(storage, cache_size)
