"""
4Suite repository low-level driver for ordinary file system storage

Copyright 2004 Fourthought, Inc. (USA).
Detailed license and copyright information: http://4suite.org/COPYRIGHT
Project home, documentation, distributions: http://4suite.org/
"""

import sys, os, types
from distutils.dir_util import remove_tree
from Ft.Rdf import Model, Statement
from Ft.Server.Common import ResourceTypes, AclConstants, Schema
from Ft.Server.Server import FtServerServerException, Error
from Ft.Server import FTSERVER_NAMESPACE
from Ft.Xml.XLink import XLINK_NAMESPACE
from Ft.Server.Server.Drivers.FtssDriver import DataStoreContainer

g_typeDirs = {ResourceTypes.RESOURCE_METADATA:'metadata',
              ResourceTypes.RESOURCE_CONTENT:'content',
              ResourceTypes.RESOURCE_CACHE:'cache',
              ResourceTypes.SESSION_DATA:'sessions',
              }
def createFlatFilePath(root, path, t):
    """
    Given the absolute path of a directory on the file system to use as
    the root container of the repository, plus a repo path and a
    resource type, returns a file system path corresponding to the
    given repo path and type.
    """
    if t == ResourceTypes.RESOURCE_CONTENT:
        path = path[1:]
    else:
        # flatten path to just a filename for metadata/cache/sessions
        path = path.replace('/', '#')

    # FIXME: Replace filesystem-unsafe characters in path segments.
    # (Mainly affects Windows). This cannot be done right now because
    # FtssDriver and PathImp cannot handle anything the least bit
    # weird in a repo path, like "<", "&", ":", "%" ...

    # Replace path separators as needed
    if os.sep != '/':
        path = path.replace('/', os.sep)

    # Prepend base dir
    path = os.path.join(root, g_typeDirs[t], path)

    return path



CONTENT_PATH = createFlatFilePath


SYSTEM_RDF_FILE = 'system-rdf'
USER_RDF_FILE = 'user-rdf'


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 DATABASE_DIR
    global RdfDbm

    from Ft import GetConfigVar
    dbdir = os.path.join(GetConfigVar('LOCALSTATEDIR'), 'FlatFile')
    dbdir = os.environ.get('FT_DATABASE_DIR', dbdir)

    # Normalize path, remove unnecessary slashes
    DATABASE_DIR = os.path.abspath(dbdir)

    # FIXME: l10n
    if not os.path.isdir(DATABASE_DIR):
        raise ValueError("Directory %r does not exist" % DATABASE_DIR)

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


def Exists(properties):
    """
    See if this repo exists.  The return value for this is three states:
    1.  Everything is present
    0.  Some things are there
    -1  Nothing is there.
    """
    InitializeModule()
    root = os.path.join(DATABASE_DIR, properties['Root'])

    systemRdfFile = os.path.join(root,SYSTEM_RDF_FILE)
    userRdfFile = os.path.join(root,USER_RDF_FILE)

    if not os.path.exists(root): return -1
    if not os.path.exists(systemRdfFile): return 0
    if not os.path.exists(userRdfFile): return 0
    return 1


def Initialize(properties):
    """
    Initialize a new copy of the repo.  This is not the same as a
    '4ss_manager init'.  This is very raw.  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.
    """

    root = os.path.join(DATABASE_DIR, properties['Root'])

    systemRdfFile = os.path.join(root, SYSTEM_RDF_FILE)
    userRdfFile = os.path.join(root, USER_RDF_FILE)

    if os.path.isdir(DATABASE_DIR):
        try:
            os.makedirs(root)
        except OSError, e:
            raise FtServerServerException(Error.UNABLE_TO_INITALIZE_REPO,
                                      msg=str(e),
                                      traceback="creating root directory")
    else:
        raise FtServerServerException(Error.UNABLE_TO_INITALIZE_REPO,
                                      msg="Directory %s does not exist" % DATABASE_DIR)


    # Create the metadata locations for each type.  We split on type now for
    # a bit more effeciency but we may want to do something later
    for name, value in g_typeDirs.items():
        path = os.path.join(root, value)
        if not os.path.exists(path):
            os.mkdir(path)

    RdfDbm.CreateDb('rdf:' + systemRdfFile)
    RdfDbm.CreateDb('rdf:' + userRdfFile)


def Destroy(properties,tryOnly=0):
    """Completly destroy the repo"""

    root = os.path.join(DATABASE_DIR,properties['Root'])

    #This is a simple as a rm -rf :)
    if os.path.exists(root):
        try:
            remove_tree(root)
        except OSError:
            if not tryOnly:
                raise
            

def Maintain(properties):
    """
    Perform any maintenance on the db
    """
    pass

import threading
g_fileLocks = {}

def GetLock(fName, typ):
    if not g_fileLocks.has_key((fName, typ)):
        g_fileLocks[(fName, typ)] = threading.RLock()
    return g_fileLocks[(fName, typ)]

class FlatFileContainer(DataStoreContainer):
    """
    Implements repository containers as directories on the underlying file system.
    Overides _createContainer to attempt an os.mkdir on the filesystem
    """
    def __init__(self,db):
        """
        Used if instanciated directly in order to do initialization
        """
        self._db = db
    
    def initialize(self):
        """
        Initialize the table
        """
        pass    
    
    def _createContainer(self,path,content):
        """
        Overidden to update _created to reflect the newly created container
        """
        path = path.absolutePath
        cPath = CONTENT_PATH(self._rootDir, path, ResourceTypes.RESOURCE_CONTENT)
        #Add entry for new container in _children cache
        self._children[path] = []
        
        #Transaction Caviat #1 (creating a container deleted within the same transaction)
        #If this container was previously marked for deletion (within the same transaction),
        #remove from _deleted otherwise add to _created
        if (path, ResourceTypes.RESOURCE_CONTENT, 1) in self._deleted:
            del self._deleted[(path, ResourceTypes.RESOURCE_CONTENT, 1)]
            try:
                del self._deleted[(path, ResourceTypes.RESOURCE_METADATA, 1)]
            except KeyError:
                pass
            try:
                del self._deleted[(path, ResourceTypes.RESOURCE_CACHE, 1)]
            except KeyError:
                pass
        else:
            self._created[(path, ResourceTypes.RESOURCE_CONTENT, 1)] = content
        return
        
    def deleteContainer(self, path):
        key = (path, ResourceTypes.RESOURCE_CONTENT, 1)
        if key in self._deleted:
            raise FtServerServerException(Error.UNKNOWN_PATH,
                                          path=path)

        if key in self._created:
            del self._created[key]            

        self._deleted[key] = 1
        return

    def fetchChildren(self, parent):
        """
        Returns a list of child uris for the given parent.  Parent is a PathImp
        Needs to hit the file system to do a directory listing of the parent PathImp as a file system path
        """
        parentPath = parent.absolutePath
        if parentPath not in self._children:
            cPath = CONTENT_PATH(self._rootDir, parentPath,
                                 ResourceTypes.RESOURCE_CONTENT)
            l = GetLock(parent, ResourceTypes.RESOURCE_CONTENT)
            l.acquire()
            try:
                children = os.listdir(cPath)
            except:
                children = []
            self._children[parentPath] = children
            l.release()
        else:
            children = self._children[parentPath]

        return children
        
    def manageChildren(self, parent, child, add=True):
        """
        Adds/remove the child specified by the given path to the parent (parent - a PathImp)
        Needs to add/remove children from _children (so they are accounted for if accessed within the same transaction)
        """
        if (parent.absolutePath, ResourceTypes.RESOURCE_CONTENT, 1) in self._deleted:
            #Transaction Caviat #2 (adding children to a container deleted previously within the same transaction)
            #Check if parent is in _deleted.  If so, raise an exception about the parent not existing
            raise FtServerServerException(Error.INVALID_PATH,
                                          path=parent.displayPath,
                                          type='Container')
        children = self.fetchChildren(parent)
        if add:
            children.append(child)
        else:
            index = children.index(child)
            del children[index]
    


class FlatFileDriver(FlatFileContainer):
    """
    The FlatFile Driver will store information in a directory tree.  This
    driver will not support any locking or concurrency.  See
    ConcurrentFlatFileDriver for information on that.

    To store information, the flat file driver will create two directories
    in its root directory.  The first will be called 'metadata' and under
    it will be a picture of the metadata in the tree.  The second will be
    called content and will contain the content of the tree.

    In the root, it will also create a file called system-rdf which is the
    RDF of the system using the Dbm driver of 4RDF.
from Ft.Server.Server.Drivers.FlatFile import 
    Also in the Root will be a file called user.rdf which is a second RDF
    Model with user information.
    """

    def __init__(self, root):
        if root[0] == '/':
            root = root[1:]
        self._rootDir = os.path.join(DATABASE_DIR, root)

        self._systemRdfFile = os.path.join(self._rootDir, SYSTEM_RDF_FILE)
        self._userRdfFile = os.path.join(self._rootDir, USER_RDF_FILE)


        self._systemRdfModel = None
        self._userRdfModel = None
        self._commited = 0

        self._created = {}
        self._read = {}
        self._modified = {}
        self._deleted = {}
        self._children = {}
        return

    def createFile(self, path, typ, content):
        """When creating a resource, 
        store the content.  """
        #Verify that we were given a string object
        if type(content) != types.StringType:
            raise TypeError("Content must be a string, not %s" % type(content))
        
        key = (path, typ, 0)
        if key in self._deleted:
            #If they recreated it, remove from deleted
            #Turn it into a modified
            del self._deleted[key]
            self._modified[(path, typ)] = content
        else:
            self._created[key] = content
        return

    def hasFile(self, path, typ):
        """See if we have any meta information about this resource"""
        #Look into created
        if self._created.has_key((path, typ,0)) or self._created.has_key((path, typ,1)):
            return 1

        #Look into Accessed
        if (path, typ) in self._read:
            return 1
           
        #Look into Modified
        if (path, typ) in self._modified:
            return 1

        #Look into deleted
        if (path, typ, 0) in self._deleted or (path, typ, 1) in self._deleted:
            return 0
        
        cPath = CONTENT_PATH(self._rootDir, path, typ)
        if os.path.exists(cPath): return 1
        return 0

    def fetchFile(self, path, typ):

        #First look in created
        if self._created.has_key((path, typ,0)):
            return self._created[(path, typ,0)]
        
        if self._created.has_key((path, typ,1)):
            return self._created[(path, typ,1)]        

        #Look into the cache
        if self._read.has_key((path, typ)):
            return self._read[(path, typ)]

        #Look into the modified cache
        if self._modified.has_key((path, typ)):
            return self._modified[(path, typ)]

        #See if it was deleted
        if (path, typ, 0) in self._deleted:
            return None

        cPath = CONTENT_PATH(self._rootDir, path, typ)
        if not os.path.exists(cPath):
            return None

        l = GetLock(path, typ)
        l.acquire()
        try:
            fd = open(cPath, 'rb')
            #Return the content as a string
            content = fd.read()
            fd.close()
            self._read[(path, typ)] = content
            return content
        finally:
            l.release()
        return

    def updateFile(self, path, typ, content):
        """Update only the content about this resource"""

        #Verify that we were given a string object
        if type(content) != types.StringType:
            raise TypeError("Content must be a string, not %s" % type(content))

        #See if it was deleted
        if (path, typ, 0) in self._deleted:
            return None

        #Clean up other caches
        if self._created.has_key((path, typ,0)):
            del self._created[(path, typ,0)]
        if self._created.has_key((path, typ,1)):
            del self._created[(path, typ,1)]            

        if self._read.has_key((path, typ)):
            del self._read[(path, typ)]

        if self._modified.has_key((path, typ)):
            del self._modified[(path, typ)]

        self._modified[(path, typ)] = content
        return 1

    def deleteFile(self, path, typ):
        """Delete an object"""

        if self._deleted.has_key((path, typ, 0)):
            raise FtServerServerException(Error.UNKNOWN_PATH,
                                          path=path)
            
        if self._created.has_key((path, typ, 0)):
            del self._created[(path, typ,0)]

        if self._read.has_key((path, typ)):
            del self._read[(path, typ)]

        if self._modified.has_key((path, typ)):
            del self._modified[(path, typ)]

        self._deleted[(path, typ, 0)] = 1
        return
       
    def getSystemModel(self):
        if not self._systemRdfModel:
            d = RdfDbm.GetDb('rdf:' + self._systemRdfFile)
            d.begin()
            self._systemRdfModel = Model.Model(d)
        return self._systemRdfModel

    def getUserModel(self):
        if not self._userRdfModel:
            d = RdfDbm.GetDb('rdf:' + self._userRdfFile)
            d.begin()
            self._userRdfModel = Model.Model(d)
        return self._userRdfModel
    
    def commit(self):
        """Make it so!"""
        if self._systemRdfModel:
            self._systemRdfModel._driver.commit()
            self._systemRdfModel = None
        if self._userRdfModel:
            self._userRdfModel._driver.commit()
            self._userRdfModel = None

        #Add any created containers
        #Containers should be created first (with os.mkdir) since the proper order cannot be guaranteed
        #/ is the only exception (should be skipped)
        #Container paths are sorted ensure proper creation order
        sortedContainerList = [key for key in self._created if key[-1]]
        sortedContainerList.sort()
        for path,typ,container in sortedContainerList:
            if path != "/":
                cPath = CONTENT_PATH(self._rootDir, path, typ)
                l = GetLock(path, ResourceTypes.RESOURCE_CONTENT)
                l.acquire()
                try:
                    os.mkdir(cPath)
                except:
                    raise FtServerServerException(Error.NOT_SUPPORTED,
                                          reason='Unable to do an os.mkdir on %s (%s) cannot proceed'%(path,cPath))
                l.release()

        #Add files
        for (path,typ,container),content in self._created.items():
            if not container:
                cPath = CONTENT_PATH(self._rootDir, path, typ)
                l = GetLock(path, typ)
                l.acquire()
                try:
                    if not os.path.isdir(cPath):
                        fd = open(cPath, 'wb')
                        fd.write(content)
                        fd.close()
                finally:
                    l.release()

        #Add any modified files
        for (path,typ),content in self._modified.items():
            cPath = CONTENT_PATH(self._rootDir, path, typ)
            l = GetLock(path, typ)
            l.acquire()
            try:
                #Special case (containers) use os.mkdir (so ignore)
                if not os.path.isdir(cPath):
                    fd = open(cPath, 'wb')
                    fd.write(content)
                    fd.close()
            finally:
                l.release()

        # Remove any deleted files
        deleted_files = [ info for info in self._deleted if not info[-1] ]
        for (path, typ, is_container) in deleted_files:
            cPath = CONTENT_PATH(self._rootDir, path, typ)
            l = GetLock(path, typ)
            l.acquire()
            try:
                os.unlink(cPath)
            finally:
                l.release()
                    
        # Containers should be deleted last (with os.rmdir) since the proper
        # order cannot be guaranteed
        deleted_dirs = [ info for info in self._deleted if info[-1] ]
        deleted_dirs.sort()
        deleted_dirs.reverse()
        for (path, typ, is_container) in deleted_dirs:
            cPath = CONTENT_PATH(self._rootDir, path, typ)
            l = GetLock(path, typ)
            l.acquire()
            try:
                os.rmdir(cPath)
            finally:
                l.release()

        self._commited = 1
        return
        
    def rollback(self):
        """Undo it"""
        if self._systemRdfModel:
            self._systemRdfModel._driver.rollback()
            self._systemRdfModel = None
        if self._userRdfModel:
            self._userRdfModel._driver.rollback()
            self._userRdfModel = None
        self._commited = 1
        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 FlatFileDriver(properties['Root'])

NAME='Flat File'

