# Copyright (c) 2009 ActiveState Software Inc.
# See http://www.activestate.com/activepython/license/ for licensing
# information.

import os
from os.path import exists, dirname

from contextlib import contextmanager

import sqlalchemy.types
from sqlalchemy import Table, Column, String, Text, MetaData
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, scoped_session, mapper

from pypm.common import console


#
# setup
#

def setup(db_class, simple_object_cls, primary_keys):
    """A simple API to configure the metadata"""
    table_name = simple_object_cls.__name__
    column_names = simple_object_cls.FIELDS
    
    metadata = MetaData()
    table = Table(table_name, metadata,
                  *[Column(cname, _get_best_column_type(cname),
                           primary_key=cname in primary_keys)
                    for cname in column_names])

    db_class.metadata = metadata
    db_class.mapper_class = simple_object_cls
    db_class.table = table

    mapper(simple_object_cls, table)
    

#
# sensible defaults for column types
#
class _get_best_column_type():
    """Return the best column type for the given name."""
    def __init__(self):
        self.mapping = {}
        # String
        for name in ['name', 'version', 'keywords', 'home_page', 'license',
                     'author', 'author_email', 'maintainer', 'maintainer_email',
                     'osarch', 'pyver', 'pkg_version', 'relpath', 'tags']:
            self.mapping[name] = String
        # Text
        for name in ['summary', 'description']:
            self.mapping[name] = Text
        # PickleType
        for name in ['install_requires', 'files_list']:
            self.mapping[name] = sqlalchemy.types.PickleType
    def __call__(self, name):
        try:
            return self.mapping[name]
        except KeyError:
            raise KeyError, \
                'missing key. add type for "{0}" in __init__'.format(
                name)
_get_best_column_type = _get_best_column_type()


# 
# sqlalchemy escape
#

def sqlalchemy_escape(val, escape_char, special_chars):
    """Escape a string according for use in LIKE operator

    >>> sqlalchemy_escape("text_table", "\\", "%_")
    'text\_table'
    """
    assert isinstance(val, basestring)
    result = []
    for c in val:
        if c in special_chars + escape_char:
            result.extend(escape_char + c)
        else:
            result.extend(c)
    return ''.join(result)


#
# main db class
# 

class SqlAlchemyDatabase(object):
    metadata = None # to be set up derived classes

    class DoesNotExist(IOError):
        def __init__(self, path):
            super(IOError, self).__init__(
                'database file %s does not exist' % path)
    
    def __init__(self, path, touch=False):
        """
        touch - create database, if it does not exist
        """
        self.path = path
        sqlite_uri = 'sqlite:///%s' % self.path
        self.engine = create_engine(sqlite_uri, echo=False)
        self.create_session = sessionmaker(
            bind=self.engine,
            autocommit=False,

            # See the comment by Michael Bayer
            # http://groups.google.com/group/sqlalchemy/browse_thread/thread/7c1eb642435adde7
            # expire_on_commit=False
        )
        self.create_scoped_session = scoped_session(self.create_session)
        
        if not exists(self.path):
            if touch:
                assert exists(dirname(self.path)), 'missing: ' + dirname(self.path)
                self.metadata.create_all(self.engine)
            else:
                raise self.DoesNotExist, path

    def reset(self):
        """Reset the database

        Drop all tables and recreate them
        """
        self.metadata.drop_all(self.engine)
        self.metadata.create_all(self.engine)

    def close(self):
        self.engine.dispose()

    @contextmanager
    def transaction(self, session=None):
        """Start a new transaction based on the passed session object. If session
        is not passed, then create one and make sure of closing it finally.
        """
        local_session = None
        if session is None:
            local_session = session = self.create_scoped_session()
        try:
            yield session
        finally:
            # Since ``local_session`` was created locally, close it here itself
            if local_session is not None:
                # but wait!
                # http://groups.google.com/group/sqlalchemy/browse_thread/thread/7c1eb642435adde7
                # To workaround this issue with sqlalchemy, we can either:
                #  1) pass the session object explicitly
                #  2) do not close the session at all (bad idea - could lead to memory leaks)
                #
                # Till pypm implements atomic transations in client.installer,
                # we retain this hack (i.e., we choose (2) for now)
                pass # local_session.close()

    def __str__(self):
        return '{0.__class__.__name__}<{0.path}>'.format(self)
