import sys, base64, types

from Ft.Rdf import Model

SYSTEM_MODEL_NAME = 'system'
USER_MODEL_NAME = 'user'

BLOB_CHUNK_LENGTH = 100000


def BuildConnectString(dbname, host, port, user, passwd):
    conn_str = "dbname=%s"%dbname
    if host:
        conn_str += " host=%s"%host
    if port != -1:
        conn_str += " port=%s"%port
    if user:
        conn_str += " user=%s"%user
    if passwd:
        conn_str += " passwd=%s"%passwd
    return str(conn_str)


def DbExists(properties):
    conn_str = BuildConnectString("template1", properties['Host'], properties['Port'],
                                  properties['User'], properties['Passwd'])
    db = psycopg.connect(conn_str)
    pg_db_name = 'ft__' + str(properties['DbName'])
    cur = db.cursor()
    cur.execute("SELECT datname FROM pg_database WHERE datname=%s", [pg_db_name])
    rt = cur.fetchone()
    db.close()
    return rt and 1 or 0


def InitializeModule():
    """
    Post-import hook to initialize module's runtime variables that are not
    required at import time, but will be needed before the module-level
    functions are called.
    """
    global psycopg
    global PsycoRdf

    # Load the required database interface module
    import psycopg

    # Incorporate the RDF driver module
    from Ft.Rdf.Drivers import Psyco as PsycoRdf
    PsycoRdf.InitializeModule()
    return


def Exists(properties):
    """See if this repo exists.  The return value for this is three state.
    1.  Everything is present
    0.  Some things are there
    -1  Nothing is there.
    """
    if DbExists(properties):
        conn_str = BuildConnectString("ft__" + properties['DbName'],
                                      properties['Host'],
                                      properties['Port'],
                                      properties['User'],
                                      properties['Passwd'])
        if not PsycoRdf.ExistsDb(conn_str, SYSTEM_MODEL_NAME):return -1
        if not PsycoRdf.ExistsDb(conn_str, USER_MODEL_NAME):return 0
        db = psycopg.connect(conn_str)
        cur = db.cursor()
        for tn in ['ftss_strings',
                   'ftss_stringdata',
                   ]:
            cur.execute("select tablename from pg_tables where tablename = %s", [tn])
            if not cur.fetchone():
                db.close()
                return 0
        db.close()
        return 1
    return -1


def Initialize(properties):
    """
    Initialize a new copy of the repo.  This is a prerequiste to a full
    4ss_manager init.  The adapter will take care of calling our other
    interfaces (createContainer, createUser, etc) with all of the
    information about the rest of the init stuff to do
    """
    pg_db_name = 'ft__' + str(properties['DbName'])
    conn_str = BuildConnectString(pg_db_name,
                                  properties['Host'],
                                  properties['Port'],
                                  properties['User'],
                                  properties['Passwd'])
    PsycoRdf.CreateDb(conn_str, SYSTEM_MODEL_NAME)
    PsycoRdf.CreateDb(conn_str, USER_MODEL_NAME)

    db = psycopg.connect(conn_str)
    cur = db.cursor()

    cur.execute('CREATE TABLE ftss_stringdata (idx INT4, index INT2, data BYTEA)')
    cur.execute('CREATE TABLE ftss_strings (idx INT4 PRIMARY KEY, path TEXT, type INT4)')
    cur.execute('CREATE SEQUENCE ftss_string_id')
    db.commit()
    db.close()
    return
   

def Destroy(properties, tryOnly=0):
    """Completly dismantle the repo"""
    pg_db_name = 'ft__' + str(properties['DbName'])
    conn_str = BuildConnectString(pg_db_name,
                                  properties['Host'],
                                  properties['Port'],
                                  properties['User'],
                                  properties['Passwd'])
    PsycoRdf.DestroyDb(conn_str, SYSTEM_MODEL_NAME)
    PsycoRdf.DestroyDb(conn_str, USER_MODEL_NAME)

    db = psycopg.connect(conn_str)
    cur = db.cursor()

    for tn in ['ftss_strings',
               'ftss_stringdata',
               ]:
        try:
            cur.execute('DROP TABLE %s'% str(tn))
        except:
            sys.stderr.write("Unable to drop table %s\n" % tn);
    try:
        cur.execute('DROP SEQUENCE ftss_string_id')
    except:
        sys.stderr.write("Unable to drop sequence %s\n" % 'string_id');
    db.commit()
    db.close()
    return

def Maintain(properties):
    """
    Perform any maintenance on the db
    """
    ###We cannot do this in a transaction!!!
    pg_db_name = 'ft__' + str(properties['DbName'])
    conn_str = BuildConnectString(pg_db_name,
                                  properties['Host'],
                                  properties['Port'],
                                  properties['User'],
                                  properties['Passwd'])


    db = psycopg.connect(conn_str)
    db.execute("VACUUM")
    db.commit()
    db.close()



class PsycoDriver:
    """Store information in a Postgres Database using PsycoPG"""

    def __init__(self, dbName, host, port, user, passwd):
        self._dbName = dbName
        self._host = host
        self._port = port
        self._user = user
        self._passwd = passwd
        self._connStr = BuildConnectString('ft__' + dbName, host, port,
                                           user, passwd)
        #FIXME: Needs refactoring to reduce connection hogging
        self._db = psycopg.connect(self._connStr)
        d = PsycoRdf.DbAdapter('', SYSTEM_MODEL_NAME)
        d._db = self._db
        d._uriHeadCache = {}
        d._resourceCache = {}
        self._systemRdfModel = Model.Model(d)
        d = PsycoRdf.DbAdapter('', USER_MODEL_NAME)
        d._db = self._db
        d._uriHeadCache = {}
        d._resourceCache = {}
        self._userRdfModel = Model.Model(d)
        return

    def maintain(self):
        """
        Perform any maintenance on the db
        """
        pass

    def commit(self):
        """Make it so!"""
        self._db.commit()
        cur = self._db.cursor()
        cur.execute("COMMIT; VACUUM ANALYZE;")
        self._db.commit()
        self._db.close()
        self._db = None
        self._systemRdfModel._db = None
        self._systemRdfModel = None
        self._userRdfModel._db = None
        self._userRdfModel = None
        return

    def rollback(self):
        """Undo it"""
        self._db.close()
        self._db = None
        self._systemRdfModel._db = None
        self._systemRdfModel = None
        self._userRdfModel._db = None
        self._userRdfModel = None
        return

    def getSystemModel(self):
        return self._systemRdfModel

    def getUserModel(self):
        return self._userRdfModel
    
    def createFile(self, path, typ, content):
        """When creating a resource, 
        store the content"""
        #if path.absolutePath == '/ftss/dashboard/manager.xslt':
        #    print "Psyco.createFile ", path, "length", len(content); import sha; print sha.new(content).hexdigest()
        #    open('/tmp/f1', 'wb').write(cPickle.dumps(cache))
        cur = self._db.cursor()
        cur.execute("SELECT NEXTVAL('ftss_string_id')")
        idx = cur.fetchone()[0]
        self._writeString(idx, content)
        cur.execute("INSERT INTO ftss_strings (idx, path, type) VALUES (%d, %s, %d)", (idx, str(path), typ))
        return

    def hasFile(self, path, typ):
        """See if we have any meta information about this resource"""
        idx = self._getId(path, typ)
        if idx: return 1
        return 0

    def fetchFile(self, path, typ):
        idx = self._getId(path, typ)
        if not idx: return None
        cur = self._db.cursor()
        cur.execute("SELECT data FROM ftss_stringdata WHERE idx = %d ORDER BY index", (idx,))
        res = cur.fetchall()
        if not res:
            return None
        res = [ r[0] for r in res ]
        data = ''.join(res)
        #if path == '/ftss/dashboard/manager.xslt':
        #    open('/tmp/f2', 'wb').write(content)
        #    print "Psyco.fetchFile result: ", len(data), ; import sha; print sha.new(data).hexdigest()
        return data

    def updateFile(self, path, typ, content):
        """Update only the content about this resource"""
        idx = self._getId(path, typ)
        if not idx: return None
        self._writeString(idx, content)
        return

    def deleteFile(self, path, typ):
        """Delete an object"""
        idx = self._getId(path, typ)
        if not idx: return None
        cur = self._db.cursor()
        cur.execute("DELETE FROM ftss_stringdata where idx = %d", [idx])
        cur.execute("DELETE FROM ftss_strings where idx = %d", [idx])
        return
       
    def _getId(self, path, typ):
        cur = self._db.cursor()
        cur.execute("SELECT idx FROM ftss_strings WHERE path = %s and type = %d",
                    (str(path), typ))
        res = cur.fetchone()
        if res:
            return int(res[0])
        return 0
        
    def _writeString(self, idx, content):
        if isinstance(content, types.UnicodeType):
            raise TypeError("_writeString content must be string, not unicode")
        cur = self._db.cursor()
        pos = 0
        size = len(content)
        chunks = []
        while pos < size:
            chunks.append(content[pos: pos + BLOB_CHUNK_LENGTH])
            pos += BLOB_CHUNK_LENGTH

        #Delete the old blob
        cur.execute("DELETE FROM ftss_stringdata where idx = %d", [idx])

        #Add the new data
        try:
            #params = [ (idx, i+1, chunks[i]) for i in range(len(chunks))]
            params = [ (idx, i+1, psycopg.Binary(chunks[i])) for i in range(len(chunks))]
            cur.executemany("INSERT INTO ftss_stringdata (idx, index, data) VALUES (%d, %d, %s)", params)
        except:
            print content
            print "*"*100
            #print repr(q)
            raise
        return


def Begin(**properties):
    """Begin a new transaction.  Every driver must support this interface.
    The properties keyword arguments are passed from the config file
    (or where ever) to the driver.  The Begin file is responsible
    for doing what ever is needed to validate these arguements
    """
    return PsycoDriver(properties['DbName'],
                       properties['Host'],
                       properties['Port'],
                       properties['User'],
                       properties['Passwd'])

NAME='Psyco'

